dsh-switchman 1.3.1 → 1.5.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.de.md +61 -29
- package/README.es.md +61 -29
- package/README.fr.md +60 -28
- package/README.it.md +53 -21
- package/README.ja.md +58 -26
- package/README.ko.md +54 -22
- package/README.md +57 -25
- package/README.pt.md +54 -22
- package/README.ru.md +61 -29
- package/README.zh-TW.md +57 -25
- package/README.zh.md +54 -22
- package/client.js +900 -17
- package/cordis.patch.yml +24 -8
- package/host/config.js +23 -0
- package/host/context-watch.js +267 -68
- package/host/dispatch.js +107 -6
- package/host/handover-state.js +38 -0
- package/host/lang.js +211 -36
- package/host/lib/dsh-home.js +18 -0
- package/host/routes.js +138 -7
- package/host/subagent-model.js +147 -0
- package/host/subagents.js +144 -0
- package/host/teams.js +430 -0
- package/host/ui-locale.js +35 -0
- package/host/vision.js +367 -0
- package/index.js +40 -8
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -4,19 +4,26 @@
|
|
|
4
4
|
* bundle per package; sibling files cannot be synchronously required).
|
|
5
5
|
*
|
|
6
6
|
* Surfaces, all reading the shared "dsh-switchman" locale namespace (zh/en):
|
|
7
|
-
* - Phase 0: the team-autonomy badge in `conversation.session.header.actions
|
|
8
|
-
*
|
|
7
|
+
* - Phase 0: the team-autonomy badge in `conversation.session.header.actions`,
|
|
8
|
+
* extended with the current session's actual model (provider/model@effort)
|
|
9
|
+
* for subagent sessions (live projection face, shared-snapshot fallback).
|
|
9
10
|
* - Phase 1: a `settings.section` page with a "Language" card:
|
|
10
11
|
* conversation / code-comment / document language preferences.
|
|
11
12
|
* - Phase 2: a "Dispatch pools & ranking" card on the same page: six lane
|
|
12
13
|
* pools and a capability ranking edited against the live model catalog
|
|
13
14
|
* (served by the Host route family), plus the enforcement mode. Every
|
|
14
|
-
* field shares one staged draft and one atomic fenced save.
|
|
15
|
+
* field shares one staged draft and one atomic fenced save. Selected
|
|
16
|
+
* routes outside the session's DSH-authorized child models (Host
|
|
17
|
+
* /authorized route) carry a ⚠ badge plus a one-line hint.
|
|
15
18
|
* - Phase 3: a "Context watermark" card on the same page: tiered token
|
|
16
19
|
* thresholds (soft/hard/force), the per-read budget, hard-tier behavior,
|
|
17
20
|
* automatic handover, and the subagent context cap — number inputs with
|
|
18
21
|
* per-field minimum plus soft<hard<force order validation, folded into
|
|
19
22
|
* the same staged draft and one atomic save.
|
|
23
|
+
* - Phase 4: an "Agent teams" card on the same page: the teams-mode master
|
|
24
|
+
* switch, the whitelist-sync sub-switch (teams mode only), and the
|
|
25
|
+
* whitelist sync status line. The ⚠ unauthorized badges and hint render
|
|
26
|
+
* only in teams mode (plain subagent dispatch names no routes).
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
window.__ModuleLoader__.load({
|
|
@@ -148,6 +155,8 @@ window.__ModuleLoader__.load({
|
|
|
148
155
|
'wmDenyMode',
|
|
149
156
|
'wmAutoHandover',
|
|
150
157
|
'wmSubagentCap',
|
|
158
|
+
'teamsMode',
|
|
159
|
+
'syncWhitelist',
|
|
151
160
|
];
|
|
152
161
|
|
|
153
162
|
/** Stable identity of one provider/model route. */
|
|
@@ -230,6 +239,10 @@ window.__ModuleLoader__.load({
|
|
|
230
239
|
const bool = (field, def) =>
|
|
231
240
|
typeof value?.[field] === 'boolean' ? value[field] : def;
|
|
232
241
|
return {
|
|
242
|
+
// 作用域枚举(host 快照缺失/非法值一律按 global 呈现);
|
|
243
|
+
// 放在首位,与 save() 组装的 desired 键序一致,保持
|
|
244
|
+
// sameValues 的无改动早退路径可用。
|
|
245
|
+
langScope: value?.langScope === 'project' ? 'project' : 'global',
|
|
233
246
|
langConversation: string('langConversation'),
|
|
234
247
|
langComments: string('langComments'),
|
|
235
248
|
langDocs: string('langDocs'),
|
|
@@ -260,6 +273,8 @@ window.__ModuleLoader__.load({
|
|
|
260
273
|
wmDenyMode: value?.wmDenyMode === 'deny' ? 'deny' : 'cap',
|
|
261
274
|
wmAutoHandover: bool('wmAutoHandover', true),
|
|
262
275
|
wmSubagentCap: bool('wmSubagentCap', true),
|
|
276
|
+
teamsMode: bool('teamsMode', false),
|
|
277
|
+
syncWhitelist: bool('syncWhitelist', false),
|
|
263
278
|
};
|
|
264
279
|
}
|
|
265
280
|
|
|
@@ -270,6 +285,7 @@ window.__ModuleLoader__.load({
|
|
|
270
285
|
* numbers — never their input-string forms). */
|
|
271
286
|
function canonical(values) {
|
|
272
287
|
return JSON.stringify({
|
|
288
|
+
langScope: values.langScope,
|
|
273
289
|
langConversation: values.langConversation,
|
|
274
290
|
langComments: values.langComments,
|
|
275
291
|
langDocs: values.langDocs,
|
|
@@ -306,6 +322,8 @@ window.__ModuleLoader__.load({
|
|
|
306
322
|
wmDenyMode: values.wmDenyMode,
|
|
307
323
|
wmAutoHandover: values.wmAutoHandover,
|
|
308
324
|
wmSubagentCap: values.wmSubagentCap,
|
|
325
|
+
teamsMode: values.teamsMode,
|
|
326
|
+
syncWhitelist: values.syncWhitelist,
|
|
309
327
|
});
|
|
310
328
|
}
|
|
311
329
|
|
|
@@ -343,9 +361,26 @@ window.__ModuleLoader__.load({
|
|
|
343
361
|
return index;
|
|
344
362
|
}
|
|
345
363
|
|
|
364
|
+
/** Parse the /authorized answer (the session's DSH-authorized child
|
|
365
|
+
* models) into { enabled, keys } — null when unknown: a failed
|
|
366
|
+
* fetch, an unreadable service (both fields null), or a malformed
|
|
367
|
+
* shape all degrade to "no badges, no hint". A readable-but-empty
|
|
368
|
+
* whitelist yields keys = empty Set (every selected route drifts). */
|
|
369
|
+
function parseAuthorized(value) {
|
|
370
|
+
if (value === null || typeof value !== 'object') return null;
|
|
371
|
+
if (value.enabled !== true && value.enabled !== false) return null;
|
|
372
|
+
if (!Array.isArray(value.routes)) return null;
|
|
373
|
+
const keys = new Set();
|
|
374
|
+
for (const route of value.routes) {
|
|
375
|
+
const clean = sanitizeRoute(route);
|
|
376
|
+
if (clean !== null) keys.add(routeKey(clean));
|
|
377
|
+
}
|
|
378
|
+
return { enabled: value.enabled, keys };
|
|
379
|
+
}
|
|
380
|
+
|
|
346
381
|
/** Checkbox rows for one pool: every live catalog model, checked or
|
|
347
|
-
*
|
|
348
|
-
*
|
|
382
|
+
* not, plus this pool's saved routes that left the catalog (still
|
|
383
|
+
* removable — mirrors the shipped model-selection page). */
|
|
349
384
|
function poolRows(pool, catalogIndex) {
|
|
350
385
|
const selected = new Set(pool.map(routeKey));
|
|
351
386
|
const rows = [];
|
|
@@ -412,11 +447,30 @@ window.__ModuleLoader__.load({
|
|
|
412
447
|
badge: '自主团队',
|
|
413
448
|
badgeTip:
|
|
414
449
|
'dsh-switchman:四个内置预设(standard / ptc / minimal / cordis)已注入自主智能体团队调度规程',
|
|
450
|
+
handoverActive: '交接进行中',
|
|
451
|
+
handoverPhaseBackup: '备份会话',
|
|
452
|
+
handoverPhaseCompacting: '压缩上下文',
|
|
453
|
+
handoverPhaseContinuation: '唤醒续接',
|
|
454
|
+
handoverActiveTip:
|
|
455
|
+
'Switchman 正在后台交接:fork 备份会话并压缩上下文,完成后自动继续任务;期间会话静止属正常现象。',
|
|
456
|
+
visionHintNeedsPool:
|
|
457
|
+
'当前模型不支持读图:请在 dsh-switchman 设置中配置「视觉池」模型,之后用 /vision 发送图片。',
|
|
458
|
+
visionHintUseVision:
|
|
459
|
+
'当前模型不支持读图:图片请用 /vision 发送(将自动派发视觉池读取)。',
|
|
460
|
+
visionAutoConverted:
|
|
461
|
+
'图片直发被拒:已自动改用 /vision 重发(由视觉池读取)。',
|
|
415
462
|
settingsTitle: 'dsh-switchman',
|
|
463
|
+
panelLabel: 'Switchman 调度中心',
|
|
416
464
|
settingsDescription: '配置 dsh-switchman 的各项偏好。',
|
|
417
465
|
langSectionTitle: '语言偏好',
|
|
418
466
|
langSectionDescription:
|
|
419
467
|
'控制智能体的对话、代码注释与文档撰写语言。',
|
|
468
|
+
langScopeLabel: '语言作用域',
|
|
469
|
+
langScopeHelp: '选择这三项语言偏好的存放位置。',
|
|
470
|
+
langScopeGlobal: '全局(本 profile)',
|
|
471
|
+
langScopeProject: '按项目(.switchman/lang.json)',
|
|
472
|
+
langProjectNote:
|
|
473
|
+
'按项目模式:各项目读取自身的 .switchman/lang.json;首次询问的答案会写入该文件,文件不存在时每个会话都会询问。以下三项仅在全局模式生效。',
|
|
420
474
|
langConversationLabel: '对话语言',
|
|
421
475
|
langConversationHelp:
|
|
422
476
|
'智能体回复与讨论使用的语言;未设置时会在首次使用时询问一次并记住。',
|
|
@@ -478,6 +532,9 @@ window.__ModuleLoader__.load({
|
|
|
478
532
|
catalogEmpty: '当前没有模型提供方公布模型。',
|
|
479
533
|
catalogUnavailableGroup: '已保存但当前不可用',
|
|
480
534
|
catalogUnavailable: '当前不可用',
|
|
535
|
+
unauthorizedBadge: '未在 DSH 设置的子智能体模型选择授权列表中',
|
|
536
|
+
unauthorizedHint:
|
|
537
|
+
'标有 ⚠ 的模型未在 DSH 设置 → 子智能体 → 模型选择 中授权,Agent 显式指定它们会被拒绝;去 DSH 设置授权,或让 Agent 走隐式派发。',
|
|
481
538
|
rankTitle: '能力排序',
|
|
482
539
|
rankDescription:
|
|
483
540
|
'序号即能力序(最强在前),可为每项锚定 S/A/B/C 档。列表为六池选中 ∪ 已有排序;任意编辑都会固化当前显示顺序。',
|
|
@@ -524,16 +581,48 @@ window.__ModuleLoader__.load({
|
|
|
524
581
|
wmSubagentFollowForce: '跟随 force 档(0)',
|
|
525
582
|
wmCommands:
|
|
526
583
|
'命令:/ctx-pause 暂停水位干预 · /ctx-resume 恢复干预 · /ctx-handover 立即备份并交接压缩。',
|
|
584
|
+
teamsSectionTitle: '智能体团队',
|
|
585
|
+
teamsSectionDescription:
|
|
586
|
+
'切换派发形态:关闭为纯 subagent 派发(DSH 出厂保守团队策略接管);开启后注入自主团队规程并启用 Agent Teams。',
|
|
587
|
+
teamsModeLabel: '智能体团队模式',
|
|
588
|
+
teamsModeHelp:
|
|
589
|
+
'开启后注入团队规程(默认委派 + 分级验证 + 任务板纪律),并自动启用 DSH 的 Agent Teams bundle;派发池的 ⚠ 授权徽标仅在开启时显示。',
|
|
590
|
+
syncWhitelistLabel: '同步子智能体模型白名单',
|
|
591
|
+
syncWhitelistHelp:
|
|
592
|
+
'独立开关(不依赖智能体团队模式):把六池选中模型的并集整体写入 DSH「设置 → 子智能体 → 模型选择」的授权列表;dsh-switchman 成为唯一事实源。',
|
|
593
|
+
whitelistSyncOk: '已同步 {count} 条({time})。',
|
|
594
|
+
whitelistSyncError: '同步失败:{error}',
|
|
595
|
+
whitelistSyncPending: '等待同步…',
|
|
596
|
+
whitelistSyncNote: '白名单变更仅对之后新建的会话生效。',
|
|
527
597
|
},
|
|
528
598
|
en: {
|
|
529
599
|
badge: 'Team autonomy',
|
|
530
600
|
badgeTip:
|
|
531
601
|
'dsh-switchman: the four built-in presets (standard / ptc / minimal / cordis) carry the autonomous Agent Teams doctrine',
|
|
602
|
+
handoverActive: 'Handover in progress',
|
|
603
|
+
handoverPhaseBackup: 'forking backup',
|
|
604
|
+
handoverPhaseCompacting: 'compacting context',
|
|
605
|
+
handoverPhaseContinuation: 'waking continuation',
|
|
606
|
+
handoverActiveTip:
|
|
607
|
+
'Switchman is handing over in the background: forking a backup session and compacting context; the task continues automatically when done. A still session during this window is expected.',
|
|
608
|
+
visionHintNeedsPool:
|
|
609
|
+
'This model cannot read images: add vision-pool models in the dsh-switchman settings, then send images with /vision.',
|
|
610
|
+
visionHintUseVision:
|
|
611
|
+
'This model cannot read images: send them with /vision (routed to the vision pool automatically).',
|
|
612
|
+
visionAutoConverted:
|
|
613
|
+
'Image send was refused: automatically resubmitting via /vision (read by the vision pool).',
|
|
532
614
|
settingsTitle: 'dsh-switchman',
|
|
615
|
+
panelLabel: 'Switchman',
|
|
533
616
|
settingsDescription: 'Configure dsh-switchman preferences.',
|
|
534
617
|
langSectionTitle: 'Language',
|
|
535
618
|
langSectionDescription:
|
|
536
619
|
'Controls the language of agent replies, code comments, and authored documents.',
|
|
620
|
+
langScopeLabel: 'Language scope',
|
|
621
|
+
langScopeHelp: 'Where these language preferences are stored.',
|
|
622
|
+
langScopeGlobal: 'Global (this profile)',
|
|
623
|
+
langScopeProject: 'Per project (.switchman/lang.json)',
|
|
624
|
+
langProjectNote:
|
|
625
|
+
'Per-project mode: every project reads its own .switchman/lang.json; first-use answers are saved there, and sessions ask until it exists. The three fields below apply only in global mode.',
|
|
537
626
|
langConversationLabel: 'Conversation language',
|
|
538
627
|
langConversationHelp:
|
|
539
628
|
'The language agents reply and discuss in; when unset, you are asked once on first use and the choice is remembered.',
|
|
@@ -596,6 +685,9 @@ window.__ModuleLoader__.load({
|
|
|
596
685
|
catalogEmpty: 'No model provider currently advertises a model.',
|
|
597
686
|
catalogUnavailableGroup: 'Saved but currently unavailable',
|
|
598
687
|
catalogUnavailable: 'currently unavailable',
|
|
688
|
+
unauthorizedBadge: 'not authorized for explicit child dispatch in DSH Settings (subagents: model selection)',
|
|
689
|
+
unauthorizedHint:
|
|
690
|
+
'Models marked ⚠ are not authorized in DSH Settings → Subagents → model selection; agents naming them explicitly will be denied — authorize them in DSH Settings, or let agents dispatch implicitly.',
|
|
599
691
|
rankTitle: 'Capability ranking',
|
|
600
692
|
rankDescription:
|
|
601
693
|
'Numbered strongest first; each entry may anchor an S/A/B/C tier. The list is pool selections ∪ the saved ranking; any edit persists the displayed order.',
|
|
@@ -642,6 +734,19 @@ window.__ModuleLoader__.load({
|
|
|
642
734
|
wmSubagentFollowForce: 'Follow the force tier (0)',
|
|
643
735
|
wmCommands:
|
|
644
736
|
'Commands: /ctx-pause suspends watermark actions · /ctx-resume resumes them · /ctx-handover backs up and hands over now.',
|
|
737
|
+
teamsSectionTitle: 'Agent teams',
|
|
738
|
+
teamsSectionDescription:
|
|
739
|
+
"Switch the dispatch shape: off is plain subagent dispatch (DSH's factory conservative team policy takes over); on injects the autonomous team doctrine and enables Agent Teams.",
|
|
740
|
+
teamsModeLabel: 'Agent teams mode',
|
|
741
|
+
teamsModeHelp:
|
|
742
|
+
'When on, the team doctrine (delegate by default + tiered verification + task-board discipline) is injected and the DSH Agent Teams bundle is enabled; the dispatch pools ⚠ authorization badges show only in this mode.',
|
|
743
|
+
syncWhitelistLabel: 'Sync the subagent model whitelist',
|
|
744
|
+
syncWhitelistHelp:
|
|
745
|
+
'Independent switch (does not require agent teams mode): writes the union of the six pools over the DSH "Settings → Subagents → model selection" authorization list; dsh-switchman becomes the single source of truth.',
|
|
746
|
+
whitelistSyncOk: 'Synced {count} entries ({time}).',
|
|
747
|
+
whitelistSyncError: 'Sync failed: {error}',
|
|
748
|
+
whitelistSyncPending: 'Waiting to sync…',
|
|
749
|
+
whitelistSyncNote: 'Whitelist changes only affect sessions created afterwards.',
|
|
645
750
|
},
|
|
646
751
|
};
|
|
647
752
|
|
|
@@ -649,15 +754,215 @@ window.__ModuleLoader__.load({
|
|
|
649
754
|
// Session-header badge (Phase 0, unchanged)
|
|
650
755
|
// ------------------------------------------------------------------
|
|
651
756
|
|
|
652
|
-
|
|
757
|
+
/** Rotating-arc spinner (SVG SMIL animateTransform): the restricted
|
|
758
|
+
* dynamic-module runtime exposes no CSS insertion here, and SMIL
|
|
759
|
+
* animates without lifecycle code or a React state loop. */
|
|
760
|
+
function HandoverSpinner() {
|
|
761
|
+
return h(
|
|
762
|
+
'svg',
|
|
763
|
+
{
|
|
764
|
+
viewBox: '0 0 16 16',
|
|
765
|
+
width: 12,
|
|
766
|
+
height: 12,
|
|
767
|
+
'aria-hidden': true,
|
|
768
|
+
style: { flex: 'none' },
|
|
769
|
+
},
|
|
770
|
+
h(
|
|
771
|
+
'circle',
|
|
772
|
+
{
|
|
773
|
+
cx: 8,
|
|
774
|
+
cy: 8,
|
|
775
|
+
r: 6,
|
|
776
|
+
fill: 'none',
|
|
777
|
+
stroke: 'currentColor',
|
|
778
|
+
strokeWidth: 2,
|
|
779
|
+
strokeDasharray: '9 19',
|
|
780
|
+
strokeLinecap: 'round',
|
|
781
|
+
},
|
|
782
|
+
h('animateTransform', {
|
|
783
|
+
attributeName: 'transform',
|
|
784
|
+
type: 'rotate',
|
|
785
|
+
from: '0 8 8',
|
|
786
|
+
to: '360 8 8',
|
|
787
|
+
dur: '1s',
|
|
788
|
+
repeatCount: 'indefinite',
|
|
789
|
+
}),
|
|
790
|
+
),
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function SwitchmanBadge({
|
|
795
|
+
locale,
|
|
796
|
+
t,
|
|
797
|
+
sessionId,
|
|
798
|
+
useProjection,
|
|
799
|
+
useSessions,
|
|
800
|
+
}) {
|
|
801
|
+
|
|
653
802
|
// Subscription only: stable snapshot via getLocale() re-renders on switch.
|
|
654
803
|
React.useSyncExternalStore(
|
|
655
804
|
(fn) => locale.subscribe(fn),
|
|
656
805
|
() => locale.getLocale(),
|
|
657
806
|
);
|
|
807
|
+
// Live handover cue: during the fork+compact window the session
|
|
808
|
+
// GUI has no motion of its own, so the badge polls the Host's
|
|
809
|
+
// handover-state route (phases recorded from runHandover's first
|
|
810
|
+
// synchronous line) and flips to an animated status chip. Fast
|
|
811
|
+
// cadence while in flight, slow otherwise; a failed fetch or an
|
|
812
|
+
// older Host reads as "no handover" and never blocks the badge.
|
|
813
|
+
const [handover, setHandover] = React.useState(null);
|
|
814
|
+
React.useEffect(() => {
|
|
815
|
+
let stopped = false;
|
|
816
|
+
let timer = null;
|
|
817
|
+
const tick = async () => {
|
|
818
|
+
let live = null;
|
|
819
|
+
// NOTE: the handover-state route is not session-scoped, so
|
|
820
|
+
// this reflects ANY in-flight handover (cross-session bleed
|
|
821
|
+
// with several open sessions) — accepted: cosmetic, and
|
|
822
|
+
// the desktop GUI renders one session header at a time.
|
|
823
|
+
try {
|
|
824
|
+
const reply = await api.get('handover-state');
|
|
825
|
+
if (reply?.ok && reply.value?.count > 0)
|
|
826
|
+
live = reply.value.handovers[0] ?? null;
|
|
827
|
+
} catch {
|
|
828
|
+
live = null;
|
|
829
|
+
}
|
|
830
|
+
if (stopped) return;
|
|
831
|
+
setHandover(live);
|
|
832
|
+
timer = setTimeout(tick, live === null ? 10_000 : 3_000);
|
|
833
|
+
};
|
|
834
|
+
void tick();
|
|
835
|
+
return () => {
|
|
836
|
+
stopped = true;
|
|
837
|
+
if (timer !== null) clearTimeout(timer);
|
|
838
|
+
};
|
|
839
|
+
}, []);
|
|
840
|
+
// ---- Session model line ----
|
|
841
|
+
// The slot is session-scoped, so `sessionId` names the open session
|
|
842
|
+
// and `useProjection` reads its live projection faces (standard kit
|
|
843
|
+
// members; both degrade to no-ops outside the kit, same guard shape
|
|
844
|
+
// as the vision dock entry). Switching sessions re-materializes the
|
|
845
|
+
// standard props, so the face re-binds and the old subscription is
|
|
846
|
+
// dropped by the hook machinery — no manual lifecycle here.
|
|
847
|
+
const projectionHook =
|
|
848
|
+
typeof useProjection === 'function' ? useProjection : () => undefined;
|
|
849
|
+
const liveSelection = projectionHook('modelSelection');
|
|
850
|
+
// NEVER touch services this plugin did not declare in inject:
|
|
851
|
+
// the dynamic-module guard THROWS on `ctx.<undeclared>` access
|
|
852
|
+
// (ctx.sessions did exactly that and the slot occurrence error
|
|
853
|
+
// boundary swallowed it — the badge silently vanished this way).
|
|
854
|
+
// Session facts arrive only through framework-bound kit hooks, and
|
|
855
|
+
// every selector must return a value-stable primitive.
|
|
856
|
+
const sessionsHook =
|
|
857
|
+
typeof useSessions === 'function' ? useSessions : () => undefined;
|
|
858
|
+
// Durable metadata separates subagent sessions from root ones
|
|
859
|
+
// (summary `origin`/`parentId`). A root session's model already has
|
|
860
|
+
// the composer's model selector, so the line stays subagent-only.
|
|
861
|
+
const subagentKey = sessionsHook((list) => {
|
|
862
|
+
if (list == null) return 'absent';
|
|
863
|
+
const summary =
|
|
864
|
+
sessionId != null ? list.byId?.[sessionId] : undefined;
|
|
865
|
+
if (summary === undefined) return 'absent';
|
|
866
|
+
return summary.origin === 'subagent' ||
|
|
867
|
+
summary.parentId !== undefined
|
|
868
|
+
? 'subagent'
|
|
869
|
+
: 'root';
|
|
870
|
+
});
|
|
871
|
+
const isSubagent = subagentKey === 'subagent';
|
|
872
|
+
// Fallback for sessions whose retained face has not carried the
|
|
873
|
+
// key yet: the manager's shared list snapshot often already holds
|
|
874
|
+
// explicitly loaded projections (sidebar-visible subagents).
|
|
875
|
+
const fallbackText = sessionsHook((list) => {
|
|
876
|
+
if (list == null) return '';
|
|
877
|
+
const selection =
|
|
878
|
+
sessionId != null
|
|
879
|
+
? list.projectionsBySession?.[sessionId]?.values
|
|
880
|
+
?.modelSelection
|
|
881
|
+
: undefined;
|
|
882
|
+
const model = selection?.next ?? selection?.lastUsed;
|
|
883
|
+
if (model == null) return '';
|
|
884
|
+
return `${model.provider}/${model.model}${
|
|
885
|
+
model.reasoningEffort ? `@${model.reasoningEffort}` : ''
|
|
886
|
+
}`;
|
|
887
|
+
});
|
|
888
|
+
const liveModel =
|
|
889
|
+
liveSelection?.next ?? liveSelection?.lastUsed ?? null;
|
|
890
|
+
const modelText =
|
|
891
|
+
liveModel == null
|
|
892
|
+
? (fallbackText ?? '')
|
|
893
|
+
: `${liveModel.provider}/${liveModel.model}${
|
|
894
|
+
liveModel.reasoningEffort
|
|
895
|
+
? `@${liveModel.reasoningEffort}`
|
|
896
|
+
: ''
|
|
897
|
+
}`;
|
|
898
|
+
const modelChip =
|
|
899
|
+
isSubagent && modelText !== ''
|
|
900
|
+
? h(
|
|
901
|
+
'span',
|
|
902
|
+
{
|
|
903
|
+
title: modelText,
|
|
904
|
+
'data-dsh-switchman': 'session-model',
|
|
905
|
+
style: {
|
|
906
|
+
borderRadius: 'var(--dsw-radius-xs)',
|
|
907
|
+
height: '22px',
|
|
908
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
909
|
+
whiteSpace: 'nowrap',
|
|
910
|
+
display: 'inline-flex',
|
|
911
|
+
alignItems: 'center',
|
|
912
|
+
gap: '4px',
|
|
913
|
+
padding: '0 8px',
|
|
914
|
+
fontSize: '12px',
|
|
915
|
+
lineHeight: '22px',
|
|
916
|
+
flex: 'none',
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
h('span', { 'aria-hidden': true, style: { flex: 'none' } }, '◇'),
|
|
920
|
+
h('span', null, modelText),
|
|
921
|
+
)
|
|
922
|
+
: null;
|
|
923
|
+
/** Append the model chip without disturbing the status chip shapes. */
|
|
924
|
+
const withModelChip = (chip) =>
|
|
925
|
+
modelChip === null ? chip : h(React.Fragment, null, chip, modelChip);
|
|
926
|
+
if (handover !== null) {
|
|
927
|
+
const phaseKey =
|
|
928
|
+
handover.phase === 'backup'
|
|
929
|
+
? 'handoverPhaseBackup'
|
|
930
|
+
: handover.phase === 'continuation'
|
|
931
|
+
? 'handoverPhaseContinuation'
|
|
932
|
+
: 'handoverPhaseCompacting';
|
|
933
|
+
const seconds = Math.max(
|
|
934
|
+
0,
|
|
935
|
+
Math.round((Date.now() - (handover.at ?? Date.now())) / 1000),
|
|
936
|
+
);
|
|
937
|
+
const status = h(
|
|
938
|
+
'span',
|
|
939
|
+
{
|
|
940
|
+
title: `${t('handoverActiveTip')}(${seconds}s)`,
|
|
941
|
+
role: 'status',
|
|
942
|
+
'data-dsh-switchman': 'handover-active',
|
|
943
|
+
style: {
|
|
944
|
+
borderRadius: 'var(--dsw-radius-xs)',
|
|
945
|
+
background: 'var(--dsw-alias-interactive-bg-hover, transparent)',
|
|
946
|
+
color: 'var(--dsw-alias-state-business-primary, inherit)',
|
|
947
|
+
height: '22px',
|
|
948
|
+
whiteSpace: 'nowrap',
|
|
949
|
+
display: 'inline-flex',
|
|
950
|
+
alignItems: 'center',
|
|
951
|
+
gap: '4px',
|
|
952
|
+
padding: '0 8px',
|
|
953
|
+
fontSize: '12px',
|
|
954
|
+
lineHeight: '22px',
|
|
955
|
+
flex: 'none',
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
h(HandoverSpinner, null),
|
|
959
|
+
h('span', null, `${t('handoverActive')} · ${t(phaseKey)}`),
|
|
960
|
+
);
|
|
961
|
+
return withModelChip(status);
|
|
962
|
+
}
|
|
658
963
|
const text = t('badge');
|
|
659
964
|
const tip = t('badgeTip');
|
|
660
|
-
|
|
965
|
+
const status = h(
|
|
661
966
|
'span',
|
|
662
967
|
{
|
|
663
968
|
title: tip,
|
|
@@ -684,6 +989,243 @@ window.__ModuleLoader__.load({
|
|
|
684
989
|
h('span', { 'aria-hidden': true, style: { flex: 'none' } }, '⚡'),
|
|
685
990
|
h('span', null, text),
|
|
686
991
|
);
|
|
992
|
+
return withModelChip(status);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Composer-dock vision entry. Two jobs:
|
|
996
|
+
*
|
|
997
|
+
* 1. Guidance chip — DSH refuses to send pasted images while the
|
|
998
|
+
* session model has no image input; switchman's unlock is the
|
|
999
|
+
* /vision command backed by the vision pool. The chip renders
|
|
1000
|
+
* whenever image support is not positively confirmed (probe
|
|
1001
|
+
* false, or null = unresolved — the common live case: the
|
|
1002
|
+
* Host-side model-info resolution often cannot resolve custom
|
|
1003
|
+
* routes), polling the Host route at a slow fixed cadence;
|
|
1004
|
+
* any failure renders nothing.
|
|
1005
|
+
* 2. Reactive auto-conversion (v1) — when a plain send is refused
|
|
1006
|
+
* host-side (session/attachment-invalid), rewrite the draft as
|
|
1007
|
+
* "/vision <original text>" and resubmit once; the claimed
|
|
1008
|
+
* command submit carries the restored draft attachments past
|
|
1009
|
+
* the model gate to the /vision handler. The refusal code is
|
|
1010
|
+
* the authoritative image-gate signal; the probe's
|
|
1011
|
+
* imageCapable only brakes the conversion when it positively
|
|
1012
|
+
* says true. Session-scoped dock entries receive the standard
|
|
1013
|
+
* kit (inputActions, useInput, useSession) automatically;
|
|
1014
|
+
* loop safety = one conversion per promptError object
|
|
1015
|
+
* identity + never converting a draft that already starts
|
|
1016
|
+
* with "/". */
|
|
1017
|
+
function VisionDockEntry({ t, sessionId, inputActions, useInput, useSession }) {
|
|
1018
|
+
const [vision, setVision] = React.useState(null);
|
|
1019
|
+
const [notice, setNotice] = React.useState(false);
|
|
1020
|
+
const handled = React.useRef(null);
|
|
1021
|
+
const latest = React.useRef({ vision: null, draft: '', attachmentIds: [] });
|
|
1022
|
+
React.useEffect(() => {
|
|
1023
|
+
let stopped = false;
|
|
1024
|
+
let timer = null;
|
|
1025
|
+
// The probe must judge the CURRENT conversation's model: pass
|
|
1026
|
+
// the session scope's identity so the Host resolves
|
|
1027
|
+
// imageCapable from that session's model selection (subagent
|
|
1028
|
+
// sessions included); without it the Host falls back to the
|
|
1029
|
+
// root session and mislabels text-only children. Restarting
|
|
1030
|
+
// the loop on `sessionId` re-probes immediately on a switch.
|
|
1031
|
+
const query =
|
|
1032
|
+
typeof sessionId === 'string' && sessionId !== ''
|
|
1033
|
+
? `?session=${encodeURIComponent(sessionId)}`
|
|
1034
|
+
: '';
|
|
1035
|
+
const tick = async () => {
|
|
1036
|
+
let next = null;
|
|
1037
|
+
try {
|
|
1038
|
+
const reply = await api.get(`vision-state${query}`);
|
|
1039
|
+
if (reply?.ok && reply.value && typeof reply.value === 'object')
|
|
1040
|
+
next = {
|
|
1041
|
+
poolConfigured: reply.value.poolConfigured === true,
|
|
1042
|
+
imageCapable: reply.value.imageCapable,
|
|
1043
|
+
};
|
|
1044
|
+
} catch {
|
|
1045
|
+
next = null;
|
|
1046
|
+
}
|
|
1047
|
+
if (stopped) return;
|
|
1048
|
+
setVision(next);
|
|
1049
|
+
timer = setTimeout(tick, 15_000);
|
|
1050
|
+
};
|
|
1051
|
+
void tick();
|
|
1052
|
+
return () => {
|
|
1053
|
+
stopped = true;
|
|
1054
|
+
if (timer !== null) clearTimeout(timer);
|
|
1055
|
+
};
|
|
1056
|
+
}, [sessionId]);
|
|
1057
|
+
// Standard kit props are guaranteed by the session scope, but the
|
|
1058
|
+
// hooks must be called unconditionally — degrade to no-op sources
|
|
1059
|
+
// if a future shell ever mounts this entry outside that scope.
|
|
1060
|
+
const inputHook =
|
|
1061
|
+
typeof useInput === 'function' ? useInput : () => () => ({ draft: '', attachmentIds: [] });
|
|
1062
|
+
const sessionHook =
|
|
1063
|
+
typeof useSession === 'function' ? useSession : () => () => null;
|
|
1064
|
+
const input = inputHook((s) => s) ?? {};
|
|
1065
|
+
const promptError = sessionHook((s) => s.promptError);
|
|
1066
|
+
latest.current = {
|
|
1067
|
+
vision,
|
|
1068
|
+
draft: typeof input.draft === 'string' ? input.draft : '',
|
|
1069
|
+
attachmentIds: Array.isArray(input.attachmentIds) ? input.attachmentIds : [],
|
|
1070
|
+
};
|
|
1071
|
+
React.useEffect(() => {
|
|
1072
|
+
if (promptError == null || promptError === handled.current) return;
|
|
1073
|
+
handled.current = promptError;
|
|
1074
|
+
if (promptError.op !== 'send') return; // stop failures are not ours
|
|
1075
|
+
const code = promptError.error?.code;
|
|
1076
|
+
if (code !== 'session/attachment-invalid' && code !== 'subagent/attachment-invalid') return;
|
|
1077
|
+
// Let InputBar's failure restore of the draft and attachments
|
|
1078
|
+
// land before reading them.
|
|
1079
|
+
const timer = setTimeout(() => {
|
|
1080
|
+
const snap = latest.current;
|
|
1081
|
+
// The refusal code below is the authoritative image-gate
|
|
1082
|
+
// signal; vision.imageCapable is only a secondary brake.
|
|
1083
|
+
// The Host probe legitimately yields null (unknown) when
|
|
1084
|
+
// its model-info resolution fails, so skip ONLY when the
|
|
1085
|
+
// probe positively says the model reads images.
|
|
1086
|
+
if (snap.vision?.imageCapable === true) return;
|
|
1087
|
+
const text = snap.draft.trim();
|
|
1088
|
+
if (text.startsWith('/')) return; // already a command path — never loop
|
|
1089
|
+
if (snap.attachmentIds.length === 0) return;
|
|
1090
|
+
if (typeof inputActions?.setDraft !== 'function' || typeof inputActions?.submit !== 'function')
|
|
1091
|
+
return;
|
|
1092
|
+
inputActions.setDraft(text === '' ? '/vision' : `/vision ${text}`);
|
|
1093
|
+
inputActions.submit();
|
|
1094
|
+
setNotice(true);
|
|
1095
|
+
setTimeout(() => setNotice(false), 6_000);
|
|
1096
|
+
}, 300);
|
|
1097
|
+
return () => clearTimeout(timer);
|
|
1098
|
+
}, [promptError, inputActions]);
|
|
1099
|
+
if (vision === null && !notice) return null;
|
|
1100
|
+
// The dock zone spans the full conversation width while the
|
|
1101
|
+
// composer card is a centered column (max-width contract). The
|
|
1102
|
+
// hero state narrows the whole stack, the active state does
|
|
1103
|
+
// not — so a bare <p> lands hard against the far left once a
|
|
1104
|
+
// conversation starts. Mirror the shell's own QueueDock column
|
|
1105
|
+
// formula (same CSS custom properties) to keep every line's
|
|
1106
|
+
// left edge tracking the input card in both states; fallback
|
|
1107
|
+
// values degrade to the shell defaults if a variable is unset.
|
|
1108
|
+
const dockColumnStyle = {
|
|
1109
|
+
boxSizing: 'border-box',
|
|
1110
|
+
width: 'calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px))',
|
|
1111
|
+
maxWidth: 'var(--dsh-composer-card-max-width, 780px)',
|
|
1112
|
+
margin: '0 auto',
|
|
1113
|
+
flex: 'none',
|
|
1114
|
+
// Keep the rhythm between the notice and hint lines equal to
|
|
1115
|
+
// the composer stack gap they no longer participate in.
|
|
1116
|
+
display: 'flex',
|
|
1117
|
+
flexDirection: 'column',
|
|
1118
|
+
gap: 'var(--dsh-composer-stack-gap, 6px)',
|
|
1119
|
+
};
|
|
1120
|
+
return h(
|
|
1121
|
+
'div',
|
|
1122
|
+
{
|
|
1123
|
+
'data-dsh-switchman': 'vision-dock-column',
|
|
1124
|
+
style: dockColumnStyle,
|
|
1125
|
+
},
|
|
1126
|
+
notice
|
|
1127
|
+
? h(
|
|
1128
|
+
'p',
|
|
1129
|
+
{
|
|
1130
|
+
'data-dsh-switchman': 'vision-auto-converted',
|
|
1131
|
+
style: {
|
|
1132
|
+
margin: '0',
|
|
1133
|
+
padding: '2px 0',
|
|
1134
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
1135
|
+
fontSize: '12px',
|
|
1136
|
+
lineHeight: '1.6',
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
'↻ ',
|
|
1140
|
+
t('visionAutoConverted'),
|
|
1141
|
+
)
|
|
1142
|
+
: null,
|
|
1143
|
+
// Hint whenever the probe cannot positively confirm image
|
|
1144
|
+
// input — false (confirmed text-only) or null (unresolved,
|
|
1145
|
+
// the common live case): /vision is harmless either way and
|
|
1146
|
+
// this chip is the only composer-surface affordance for it.
|
|
1147
|
+
vision !== null && vision.imageCapable !== true
|
|
1148
|
+
? h(
|
|
1149
|
+
'p',
|
|
1150
|
+
{
|
|
1151
|
+
'data-dsh-switchman': 'vision-hint',
|
|
1152
|
+
style: {
|
|
1153
|
+
margin: '0',
|
|
1154
|
+
padding: '2px 0',
|
|
1155
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
1156
|
+
fontSize: '12px',
|
|
1157
|
+
lineHeight: '1.6',
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
'⚠ ',
|
|
1161
|
+
t(vision.poolConfigured ? 'visionHintUseVision' : 'visionHintNeedsPool'),
|
|
1162
|
+
)
|
|
1163
|
+
: null,
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/** Home-sidebar panel icon (sidebar.panellist contract): the shell
|
|
1168
|
+
* row renderer passes { size, active } and colors the glyph via the
|
|
1169
|
+
* row's currentColor, so only size matters here. 16×16 stroke
|
|
1170
|
+
* glyph — one dispatch stem splitting into two routed lanes.
|
|
1171
|
+
* data-dsh-panel-entry is the documented L2 skins hook (the shell
|
|
1172
|
+
* row itself carries no per-entry marker). */
|
|
1173
|
+
function SwitchmanPanelIcon({ size }) {
|
|
1174
|
+
return h(
|
|
1175
|
+
'svg',
|
|
1176
|
+
{
|
|
1177
|
+
'data-dsh-panel-entry': 'dsh-switchman',
|
|
1178
|
+
viewBox: '0 0 16 16',
|
|
1179
|
+
width: size,
|
|
1180
|
+
height: size,
|
|
1181
|
+
fill: 'none',
|
|
1182
|
+
stroke: 'currentColor',
|
|
1183
|
+
strokeWidth: 1.3,
|
|
1184
|
+
strokeLinecap: 'round',
|
|
1185
|
+
strokeLinejoin: 'round',
|
|
1186
|
+
'aria-hidden': true,
|
|
1187
|
+
},
|
|
1188
|
+
h('path', { d: 'M2 8h4.5' }),
|
|
1189
|
+
h('path', { d: 'M6.5 8 10.5 4H12' }),
|
|
1190
|
+
h('path', { d: 'M6.5 8l4 4H12' }),
|
|
1191
|
+
h('path', { d: 'M12.2 2.2 14 4l-1.8 1.8' }),
|
|
1192
|
+
h('path', { d: 'M12.2 10.2 14 12l-1.8 1.8' }),
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** Central-panel host for the sidebar entry: reuses the settings
|
|
1197
|
+
* page component unchanged (its {t, locale} props arrive as the
|
|
1198
|
+
* register inject() face). The main area hands the page a
|
|
1199
|
+
* definite-height, overflow-clipped frame with no gutter — the
|
|
1200
|
+
* page must own its scrolling (skill-explorer's .view/.tabBody
|
|
1201
|
+
* pattern): height:100% scroll owner outside, reading-width
|
|
1202
|
+
* centered column inside. */
|
|
1203
|
+
function SwitchmanPanelPage(props) {
|
|
1204
|
+
return h(
|
|
1205
|
+
'div',
|
|
1206
|
+
{
|
|
1207
|
+
'data-dsh-switchman': 'panel',
|
|
1208
|
+
style: {
|
|
1209
|
+
minWidth: 0,
|
|
1210
|
+
minHeight: 0,
|
|
1211
|
+
height: '100%',
|
|
1212
|
+
overflowY: 'auto',
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
h(
|
|
1216
|
+
'div',
|
|
1217
|
+
{
|
|
1218
|
+
style: {
|
|
1219
|
+
minWidth: 0,
|
|
1220
|
+
maxWidth: '860px',
|
|
1221
|
+
margin: '0 auto',
|
|
1222
|
+
padding: '16px 24px',
|
|
1223
|
+
boxSizing: 'border-box',
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
h(SwitchmanSettingsPage, props),
|
|
1227
|
+
),
|
|
1228
|
+
);
|
|
687
1229
|
}
|
|
688
1230
|
|
|
689
1231
|
// ------------------------------------------------------------------
|
|
@@ -834,7 +1376,7 @@ window.__ModuleLoader__.load({
|
|
|
834
1376
|
cursor: 'pointer',
|
|
835
1377
|
},
|
|
836
1378
|
poolList: {
|
|
837
|
-
maxHeight: '
|
|
1379
|
+
maxHeight: '320px',
|
|
838
1380
|
overflowY: 'auto',
|
|
839
1381
|
display: 'grid',
|
|
840
1382
|
gap: '2px',
|
|
@@ -867,6 +1409,10 @@ window.__ModuleLoader__.load({
|
|
|
867
1409
|
color: 'var(--dsw-alias-label-tertiary)',
|
|
868
1410
|
flex: 'none',
|
|
869
1411
|
},
|
|
1412
|
+
unauthorizedMark: {
|
|
1413
|
+
color: 'var(--dsw-alias-state-error-primary)',
|
|
1414
|
+
flex: 'none',
|
|
1415
|
+
},
|
|
870
1416
|
rankList: { display: 'grid', gap: '2px', marginTop: '8px' },
|
|
871
1417
|
rankRow: {
|
|
872
1418
|
display: 'grid',
|
|
@@ -885,9 +1431,12 @@ window.__ModuleLoader__.load({
|
|
|
885
1431
|
color: 'var(--dsw-alias-label-primary)',
|
|
886
1432
|
fontSize: '12.5px',
|
|
887
1433
|
lineHeight: 1.6,
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1434
|
+
// Wrap instead of ellipsize: provider/model pairs must stay
|
|
1435
|
+
// fully readable in the narrow panel (the hover title remains
|
|
1436
|
+
// a convenience, not the only way to read the full route).
|
|
1437
|
+
whiteSpace: 'normal',
|
|
1438
|
+
wordBreak: 'break-word',
|
|
1439
|
+
minWidth: 0,
|
|
891
1440
|
},
|
|
892
1441
|
rankActions: { display: 'flex', gap: '4px', flex: 'none' },
|
|
893
1442
|
effortSelect: {
|
|
@@ -1002,6 +1551,8 @@ window.__ModuleLoader__.load({
|
|
|
1002
1551
|
efforts,
|
|
1003
1552
|
manual,
|
|
1004
1553
|
editable,
|
|
1554
|
+
authorized,
|
|
1555
|
+
teamsMode,
|
|
1005
1556
|
onToggle,
|
|
1006
1557
|
onToggleManual,
|
|
1007
1558
|
onSetEffort,
|
|
@@ -1011,6 +1562,26 @@ window.__ModuleLoader__.load({
|
|
|
1011
1562
|
onClear,
|
|
1012
1563
|
}) {
|
|
1013
1564
|
const hasCandidates = rows.length > 0 || extras.length > 0;
|
|
1565
|
+
/** 授权漂移判定:该路由不在本会话 DSH 授权的子智能体模型内
|
|
1566
|
+
* (authorized === null = 未知,不标)。enabled === false 时
|
|
1567
|
+
* 一切显式指定都会被拒,所有已选路由都算漂移。仅团队模式
|
|
1568
|
+
* 显示:纯 subagent 模式不做显式路由注入,徽标无意义。 */
|
|
1569
|
+
const drift = (route) =>
|
|
1570
|
+
teamsMode === true &&
|
|
1571
|
+
authorized !== null &&
|
|
1572
|
+
(authorized.enabled === false ||
|
|
1573
|
+
!authorized.keys.has(routeKey(route)));
|
|
1574
|
+
/** 漂移行的 ⚠ 徽标(悬停/读屏文案见 unauthorizedBadge)。 */
|
|
1575
|
+
const unauthorizedBadge = () =>
|
|
1576
|
+
h(
|
|
1577
|
+
'span',
|
|
1578
|
+
{
|
|
1579
|
+
style: STYLE.unauthorizedMark,
|
|
1580
|
+
title: t('unauthorizedBadge'),
|
|
1581
|
+
'aria-label': t('unauthorizedBadge'),
|
|
1582
|
+
},
|
|
1583
|
+
'⚠',
|
|
1584
|
+
);
|
|
1014
1585
|
/** 显示名查找:覆盖目录行与已存离架行。 */
|
|
1015
1586
|
const nameByKey = new Map(
|
|
1016
1587
|
[...rows, ...extras]
|
|
@@ -1134,6 +1705,7 @@ window.__ModuleLoader__.load({
|
|
|
1134
1705
|
},
|
|
1135
1706
|
nameByKey.get(routeKey(route)) ??
|
|
1136
1707
|
`${route.provider} · ${route.model}`,
|
|
1708
|
+
drift(route) ? unauthorizedBadge() : null,
|
|
1137
1709
|
),
|
|
1138
1710
|
effortSelect(route),
|
|
1139
1711
|
h(
|
|
@@ -1299,6 +1871,9 @@ window.__ModuleLoader__.load({
|
|
|
1299
1871
|
`${row.providerName} · ${row.modelName}`,
|
|
1300
1872
|
),
|
|
1301
1873
|
...(row.selected ? [effortSelect(row)] : []),
|
|
1874
|
+
...(row.selected && drift(row)
|
|
1875
|
+
? [unauthorizedBadge()]
|
|
1876
|
+
: []),
|
|
1302
1877
|
),
|
|
1303
1878
|
),
|
|
1304
1879
|
extras.length > 0
|
|
@@ -1335,6 +1910,7 @@ window.__ModuleLoader__.load({
|
|
|
1335
1910
|
{ style: STYLE.unavailableMark },
|
|
1336
1911
|
` · ${t('catalogUnavailable')}`,
|
|
1337
1912
|
),
|
|
1913
|
+
drift(row) ? unauthorizedBadge() : null,
|
|
1338
1914
|
),
|
|
1339
1915
|
),
|
|
1340
1916
|
)
|
|
@@ -1368,6 +1944,10 @@ window.__ModuleLoader__.load({
|
|
|
1368
1944
|
const [saving, setSaving] = React.useState(false);
|
|
1369
1945
|
const [failed, setFailed] = React.useState(false);
|
|
1370
1946
|
const [conflicted, setConflicted] = React.useState(false);
|
|
1947
|
+
/** Last whitelist sync status from the Host config answer: null when
|
|
1948
|
+
* both teams switches are off (the Host omits the field), otherwise
|
|
1949
|
+
* { ok, count, at, error? }. */
|
|
1950
|
+
const [whitelistSync, setWhitelistSync] = React.useState(null);
|
|
1371
1951
|
|
|
1372
1952
|
// Mirror the editable state for async save flows below (setState
|
|
1373
1953
|
// alone is too late inside stale closures).
|
|
@@ -1377,6 +1957,20 @@ window.__ModuleLoader__.load({
|
|
|
1377
1957
|
/** Values this page loaded — the optimistic-concurrency baseline the
|
|
1378
1958
|
* Host route fences writes against. */
|
|
1379
1959
|
const loadedRef = React.useRef(null);
|
|
1960
|
+
/** Unmount guard for the post-save sync-status re-reads. */
|
|
1961
|
+
const syncAlive = React.useRef(true);
|
|
1962
|
+
React.useEffect(() => () => {
|
|
1963
|
+
syncAlive.current = false;
|
|
1964
|
+
}, []);
|
|
1965
|
+
|
|
1966
|
+
/** Re-read the config answer for the whitelist sync status line: the
|
|
1967
|
+
* Host syncs after a successful write, so one immediate and one
|
|
1968
|
+
* delayed read bracket the async window. */
|
|
1969
|
+
const refreshWhitelistSync = React.useCallback(async () => {
|
|
1970
|
+
const response = await api.get('config');
|
|
1971
|
+
if (!syncAlive.current || !response.ok) return;
|
|
1972
|
+
setWhitelistSync(response.value?.whitelistSync ?? null);
|
|
1973
|
+
}, []);
|
|
1380
1974
|
|
|
1381
1975
|
const assignDraft = (next) => {
|
|
1382
1976
|
draftRef.current = next;
|
|
@@ -1392,6 +1986,9 @@ window.__ModuleLoader__.load({
|
|
|
1392
1986
|
if (response.ok && response.value?.values !== undefined) {
|
|
1393
1987
|
loadedRef.current = response.value.values;
|
|
1394
1988
|
setForm({ status: 'ready', writable: true, values: response.value.values });
|
|
1989
|
+
// Whitelist sync status travels beside the values (null
|
|
1990
|
+
// unless both teams switches are on).
|
|
1991
|
+
setWhitelistSync(response.value.whitelistSync ?? null);
|
|
1395
1992
|
} else setForm({ status: 'error', writable: false });
|
|
1396
1993
|
})();
|
|
1397
1994
|
return () => {
|
|
@@ -1432,6 +2029,25 @@ window.__ModuleLoader__.load({
|
|
|
1432
2029
|
}, [loadCatalog]);
|
|
1433
2030
|
const catalogIndex = indexCatalog(catalog.groups);
|
|
1434
2031
|
|
|
2032
|
+
// DSH-side child-model authorization (which routes this session may
|
|
2033
|
+
// name explicitly on subagent/workflow calls): loaded once on mount
|
|
2034
|
+
// beside the catalog. null (failed fetch or unreadable service)
|
|
2035
|
+
// means unknown — no ⚠ badges, no hint.
|
|
2036
|
+
const [authorized, setAuthorized] = React.useState(null);
|
|
2037
|
+
React.useEffect(() => {
|
|
2038
|
+
let alive = true;
|
|
2039
|
+
void (async () => {
|
|
2040
|
+
const response = await api.get('authorized');
|
|
2041
|
+
if (!alive) return;
|
|
2042
|
+
setAuthorized(
|
|
2043
|
+
response.ok ? parseAuthorized(response.value) : null,
|
|
2044
|
+
);
|
|
2045
|
+
})();
|
|
2046
|
+
return () => {
|
|
2047
|
+
alive = false;
|
|
2048
|
+
};
|
|
2049
|
+
}, []);
|
|
2050
|
+
|
|
1435
2051
|
// Numeric watermark editing: one raw-text overlay per field so
|
|
1436
2052
|
// partial input ('', '5', leading zeros) is editable without
|
|
1437
2053
|
// corrupting the draft; parseable integers commit immediately,
|
|
@@ -1467,7 +2083,9 @@ window.__ModuleLoader__.load({
|
|
|
1467
2083
|
!(values.wmSoftTokens < values.wmHardTokens) ||
|
|
1468
2084
|
!(values.wmHardTokens < values.wmForceTokens);
|
|
1469
2085
|
const invalid =
|
|
1470
|
-
(
|
|
2086
|
+
(values.langScope === 'global' &&
|
|
2087
|
+
customActive &&
|
|
2088
|
+
values.langConversation.trim() === '') ||
|
|
1471
2089
|
anyNumberInvalid ||
|
|
1472
2090
|
orderInvalid;
|
|
1473
2091
|
const dirty = !sameValues(current, values);
|
|
@@ -1653,6 +2271,7 @@ window.__ModuleLoader__.load({
|
|
|
1653
2271
|
if (form.status !== 'ready' || !form.writable || savingRef.current)
|
|
1654
2272
|
return;
|
|
1655
2273
|
const desired = {
|
|
2274
|
+
langScope: values.langScope,
|
|
1656
2275
|
langConversation: values.langConversation.trim(),
|
|
1657
2276
|
langComments: values.langComments,
|
|
1658
2277
|
langDocs: values.langDocs,
|
|
@@ -1689,6 +2308,8 @@ window.__ModuleLoader__.load({
|
|
|
1689
2308
|
wmDenyMode: values.wmDenyMode,
|
|
1690
2309
|
wmAutoHandover: values.wmAutoHandover,
|
|
1691
2310
|
wmSubagentCap: values.wmSubagentCap,
|
|
2311
|
+
teamsMode: values.teamsMode,
|
|
2312
|
+
syncWhitelist: values.syncWhitelist,
|
|
1692
2313
|
};
|
|
1693
2314
|
if (sameValues(current, desired)) {
|
|
1694
2315
|
// Whitespace-only differences: nothing to write, close the draft.
|
|
@@ -1723,6 +2344,17 @@ window.__ModuleLoader__.load({
|
|
|
1723
2344
|
writable: true,
|
|
1724
2345
|
values: response.value.values,
|
|
1725
2346
|
});
|
|
2347
|
+
// The write triggers the whitelist sync Host-side; the answer
|
|
2348
|
+
// may already carry fresh status, and the delayed re-read
|
|
2349
|
+
// covers the async tail.
|
|
2350
|
+
if (response.value?.whitelistSync !== undefined)
|
|
2351
|
+
setWhitelistSync(response.value.whitelistSync);
|
|
2352
|
+
if (desired.syncWhitelist === true) {
|
|
2353
|
+
void refreshWhitelistSync();
|
|
2354
|
+
setTimeout(() => {
|
|
2355
|
+
void refreshWhitelistSync();
|
|
2356
|
+
}, 2000);
|
|
2357
|
+
}
|
|
1726
2358
|
assignDraft(null);
|
|
1727
2359
|
setCustomArmed(false);
|
|
1728
2360
|
clearNumText();
|
|
@@ -1770,6 +2402,9 @@ window.__ModuleLoader__.load({
|
|
|
1770
2402
|
const conversationId = `${headingId}-conversation`;
|
|
1771
2403
|
const commentsId = `${headingId}-comments`;
|
|
1772
2404
|
const docsId = `${headingId}-docs`;
|
|
2405
|
+
const scopeId = `${headingId}-scope`;
|
|
2406
|
+
/** Project scope moves the three slots into per-project files. */
|
|
2407
|
+
const langLocked = values.langScope === 'project';
|
|
1773
2408
|
const suggestion = suggestFromUiLocale(locale.getLocale().active);
|
|
1774
2409
|
|
|
1775
2410
|
/** Status lines under one row's control. */
|
|
@@ -1805,12 +2440,12 @@ window.__ModuleLoader__.load({
|
|
|
1805
2440
|
return lines;
|
|
1806
2441
|
};
|
|
1807
2442
|
|
|
1808
|
-
/** Shared select props (disabled while saving / read-only). */
|
|
1809
|
-
const selectProps = (id, value, onChange) => ({
|
|
2443
|
+
/** Shared select props (disabled while saving / read-only / locked). */
|
|
2444
|
+
const selectProps = (id, value, onChange, locked = false) => ({
|
|
1810
2445
|
id,
|
|
1811
2446
|
value,
|
|
1812
2447
|
onChange: (event) => onChange(event.target.value),
|
|
1813
|
-
disabled: !editable,
|
|
2448
|
+
disabled: !editable || locked,
|
|
1814
2449
|
style: STYLE.control,
|
|
1815
2450
|
});
|
|
1816
2451
|
|
|
@@ -1825,6 +2460,7 @@ window.__ModuleLoader__.load({
|
|
|
1825
2460
|
? CUSTOM
|
|
1826
2461
|
: values.langConversation,
|
|
1827
2462
|
selectConversation,
|
|
2463
|
+
langLocked,
|
|
1828
2464
|
),
|
|
1829
2465
|
...languageOptions(t, { withUnset: 'ask', withCustom: true }),
|
|
1830
2466
|
),
|
|
@@ -1836,7 +2472,7 @@ window.__ModuleLoader__.load({
|
|
|
1836
2472
|
edit('langConversation', event.target.value),
|
|
1837
2473
|
placeholder: t('customLanguagePlaceholder'),
|
|
1838
2474
|
maxLength: LANG_MAX,
|
|
1839
|
-
disabled: !editable,
|
|
2475
|
+
disabled: !editable || langLocked,
|
|
1840
2476
|
'aria-label': t('customLanguage'),
|
|
1841
2477
|
style: STYLE.control,
|
|
1842
2478
|
})
|
|
@@ -1864,9 +2500,30 @@ window.__ModuleLoader__.load({
|
|
|
1864
2500
|
enforce: 'enforceHelpEnforce',
|
|
1865
2501
|
};
|
|
1866
2502
|
const display = rankDisplay(values, catalogIndex);
|
|
2503
|
+
// Teams mode (draft value, consistent with the rest of the page):
|
|
2504
|
+
// gates the ⚠ authorization badges/hint — plain subagent dispatch
|
|
2505
|
+
// names no explicit routes, so drift marks would be noise.
|
|
2506
|
+
const teamsMode = values.teamsMode === true;
|
|
2507
|
+
/** Authorization drift exists when at least one selected pool
|
|
2508
|
+
* route is outside the DSH-authorized child models (or the
|
|
2509
|
+
* whitelist is readable and explicit selection is disabled
|
|
2510
|
+
* while any pool is configured) — drives the section hint.
|
|
2511
|
+
* Teams mode only (see above). */
|
|
2512
|
+
const anyUnauthorized =
|
|
2513
|
+
teamsMode &&
|
|
2514
|
+
authorized !== null &&
|
|
2515
|
+
(authorized.enabled === false
|
|
2516
|
+
? poolsSet > 0
|
|
2517
|
+
: POOL_FIELDS.some(({ field }) =>
|
|
2518
|
+
values[field].some(
|
|
2519
|
+
(route) => !authorized.keys.has(routeKey(route)),
|
|
2520
|
+
),
|
|
2521
|
+
));
|
|
1867
2522
|
|
|
1868
2523
|
// ---- watermark section derived state ----
|
|
1869
2524
|
const wmId = `${headingId}-wm`;
|
|
2525
|
+
// ---- teams section ids ----
|
|
2526
|
+
const teamsId = `${headingId}-teams`;
|
|
1870
2527
|
|
|
1871
2528
|
/** One watermark number input: raw-text overlay over the draft
|
|
1872
2529
|
* value (0 renders as the placeholder for the follow-force row),
|
|
@@ -1955,6 +2612,20 @@ window.__ModuleLoader__.load({
|
|
|
1955
2612
|
)
|
|
1956
2613
|
: null;
|
|
1957
2614
|
|
|
2615
|
+
/** Keys of every route configured across the six pools: the drift
|
|
2616
|
+
* badge is pool-scoped, so rank-only modelRank entries (in no
|
|
2617
|
+
* pool) never carry it. */
|
|
2618
|
+
const poolRouteKeys = new Set(
|
|
2619
|
+
POOL_FIELDS.flatMap(({ field }) => values[field].map(routeKey)),
|
|
2620
|
+
);
|
|
2621
|
+
/** Pool-scoped drift check for rank rows (same semantics as the
|
|
2622
|
+
* PoolCard drift helper, which is not in scope here). Teams mode
|
|
2623
|
+
* only, matching the pool cards. */
|
|
2624
|
+
const rankDrift = (route) =>
|
|
2625
|
+
teamsMode === true &&
|
|
2626
|
+
authorized !== null &&
|
|
2627
|
+
(authorized.enabled === false ||
|
|
2628
|
+
!authorized.keys.has(routeKey(route)));
|
|
1958
2629
|
/** One ranking row: index, label, tier select, move/remove buttons. */
|
|
1959
2630
|
const rankRow = (entry, index) => {
|
|
1960
2631
|
const key = routeKey(entry);
|
|
@@ -1980,6 +2651,17 @@ window.__ModuleLoader__.load({
|
|
|
1980
2651
|
` · ${t('catalogUnavailable')}`,
|
|
1981
2652
|
)
|
|
1982
2653
|
: null,
|
|
2654
|
+
poolRouteKeys.has(key) && rankDrift(entry)
|
|
2655
|
+
? h(
|
|
2656
|
+
'span',
|
|
2657
|
+
{
|
|
2658
|
+
style: STYLE.unauthorizedMark,
|
|
2659
|
+
title: t('unauthorizedBadge'),
|
|
2660
|
+
'aria-label': t('unauthorizedBadge'),
|
|
2661
|
+
},
|
|
2662
|
+
'⚠',
|
|
2663
|
+
)
|
|
2664
|
+
: null,
|
|
1983
2665
|
),
|
|
1984
2666
|
h(
|
|
1985
2667
|
'select',
|
|
@@ -2069,6 +2751,34 @@ window.__ModuleLoader__.load({
|
|
|
2069
2751
|
h(
|
|
2070
2752
|
'div',
|
|
2071
2753
|
{ style: STYLE.rows },
|
|
2754
|
+
h(
|
|
2755
|
+
SettingsRow,
|
|
2756
|
+
{
|
|
2757
|
+
key: 'scope',
|
|
2758
|
+
id: scopeId,
|
|
2759
|
+
label: t('langScopeLabel'),
|
|
2760
|
+
help: t('langScopeHelp'),
|
|
2761
|
+
control: h(
|
|
2762
|
+
'select',
|
|
2763
|
+
selectProps(scopeId, values.langScope, (value) =>
|
|
2764
|
+
edit('langScope', value),
|
|
2765
|
+
),
|
|
2766
|
+
h(
|
|
2767
|
+
'option',
|
|
2768
|
+
{ key: 'global', value: 'global' },
|
|
2769
|
+
t('langScopeGlobal'),
|
|
2770
|
+
),
|
|
2771
|
+
h(
|
|
2772
|
+
'option',
|
|
2773
|
+
{ key: 'project', value: 'project' },
|
|
2774
|
+
t('langScopeProject'),
|
|
2775
|
+
),
|
|
2776
|
+
),
|
|
2777
|
+
lines: langLocked
|
|
2778
|
+
? [{ text: t('langProjectNote'), invalid: false }]
|
|
2779
|
+
: [],
|
|
2780
|
+
},
|
|
2781
|
+
),
|
|
2072
2782
|
h(
|
|
2073
2783
|
SettingsRow,
|
|
2074
2784
|
{
|
|
@@ -2093,6 +2803,7 @@ window.__ModuleLoader__.load({
|
|
|
2093
2803
|
commentsId,
|
|
2094
2804
|
values.langComments,
|
|
2095
2805
|
(value) => edit('langComments', value),
|
|
2806
|
+
langLocked,
|
|
2096
2807
|
),
|
|
2097
2808
|
...languageOptions(t, { withUnset: 'follow' }),
|
|
2098
2809
|
),
|
|
@@ -2112,6 +2823,7 @@ window.__ModuleLoader__.load({
|
|
|
2112
2823
|
docsId,
|
|
2113
2824
|
values.langDocs,
|
|
2114
2825
|
(value) => edit('langDocs', value),
|
|
2826
|
+
langLocked,
|
|
2115
2827
|
),
|
|
2116
2828
|
...languageOptions(t, { withUnset: 'follow' }),
|
|
2117
2829
|
),
|
|
@@ -2146,6 +2858,13 @@ window.__ModuleLoader__.load({
|
|
|
2146
2858
|
}),
|
|
2147
2859
|
),
|
|
2148
2860
|
catalogNotice,
|
|
2861
|
+
anyUnauthorized
|
|
2862
|
+
? h(
|
|
2863
|
+
'p',
|
|
2864
|
+
{ key: 'unauthorized', role: 'status', style: STYLE.progress },
|
|
2865
|
+
t('unauthorizedHint'),
|
|
2866
|
+
)
|
|
2867
|
+
: null,
|
|
2149
2868
|
h(
|
|
2150
2869
|
'div',
|
|
2151
2870
|
{ style: STYLE.poolGrid },
|
|
@@ -2164,6 +2883,8 @@ window.__ModuleLoader__.load({
|
|
|
2164
2883
|
efforts: values[effortsField],
|
|
2165
2884
|
manual: values[manualField],
|
|
2166
2885
|
editable,
|
|
2886
|
+
authorized,
|
|
2887
|
+
teamsMode,
|
|
2167
2888
|
onToggle: (meta) => {
|
|
2168
2889
|
togglePool(field, meta);
|
|
2169
2890
|
},
|
|
@@ -2333,6 +3054,102 @@ window.__ModuleLoader__.load({
|
|
|
2333
3054
|
),
|
|
2334
3055
|
h('p', { style: STYLE.progress }, t('wmCommands')),
|
|
2335
3056
|
),
|
|
3057
|
+
h(
|
|
3058
|
+
'section',
|
|
3059
|
+
{
|
|
3060
|
+
'aria-labelledby': teamsId,
|
|
3061
|
+
style: { ...STYLE.section, ...STYLE.sectionDivider },
|
|
3062
|
+
},
|
|
3063
|
+
h(
|
|
3064
|
+
'h3',
|
|
3065
|
+
{ id: teamsId, style: STYLE.heading },
|
|
3066
|
+
t('teamsSectionTitle'),
|
|
3067
|
+
),
|
|
3068
|
+
h(
|
|
3069
|
+
'p',
|
|
3070
|
+
{ style: STYLE.description },
|
|
3071
|
+
t('teamsSectionDescription'),
|
|
3072
|
+
),
|
|
3073
|
+
h(
|
|
3074
|
+
'div',
|
|
3075
|
+
{ style: STYLE.rows },
|
|
3076
|
+
h(SettingsRow, {
|
|
3077
|
+
key: 'teamsMode',
|
|
3078
|
+
id: `${teamsId}-mode`,
|
|
3079
|
+
label: t('teamsModeLabel'),
|
|
3080
|
+
help: t('teamsModeHelp'),
|
|
3081
|
+
control: h('input', {
|
|
3082
|
+
type: 'checkbox',
|
|
3083
|
+
id: `${teamsId}-mode`,
|
|
3084
|
+
checked: values.teamsMode,
|
|
3085
|
+
onChange: () => {
|
|
3086
|
+
edit('teamsMode', !values.teamsMode);
|
|
3087
|
+
},
|
|
3088
|
+
disabled: !editable,
|
|
3089
|
+
style: STYLE.checkbox,
|
|
3090
|
+
}),
|
|
3091
|
+
lines: [],
|
|
3092
|
+
}),
|
|
3093
|
+
h(SettingsRow, {
|
|
3094
|
+
key: 'syncWhitelist',
|
|
3095
|
+
id: `${teamsId}-sync`,
|
|
3096
|
+
label: t('syncWhitelistLabel'),
|
|
3097
|
+
help: t('syncWhitelistHelp'),
|
|
3098
|
+
control: h('input', {
|
|
3099
|
+
type: 'checkbox',
|
|
3100
|
+
id: `${teamsId}-sync`,
|
|
3101
|
+
checked: values.syncWhitelist,
|
|
3102
|
+
onChange: () => {
|
|
3103
|
+
edit('syncWhitelist', !values.syncWhitelist);
|
|
3104
|
+
},
|
|
3105
|
+
disabled: !editable,
|
|
3106
|
+
style: STYLE.checkbox,
|
|
3107
|
+
}),
|
|
3108
|
+
// Status line reads the Host-reported sync state;
|
|
3109
|
+
// visible whenever the (independent) sync switch
|
|
3110
|
+
// is on — the only shape the Host reports for.
|
|
3111
|
+
lines:
|
|
3112
|
+
values.syncWhitelist === true
|
|
3113
|
+
? [
|
|
3114
|
+
whitelistSync == null
|
|
3115
|
+
? {
|
|
3116
|
+
text: t('whitelistSyncPending'),
|
|
3117
|
+
invalid: false,
|
|
3118
|
+
}
|
|
3119
|
+
: whitelistSync.ok === true
|
|
3120
|
+
? {
|
|
3121
|
+
text: t('whitelistSyncOk', {
|
|
3122
|
+
count: whitelistSync.count ?? 0,
|
|
3123
|
+
time:
|
|
3124
|
+
typeof whitelistSync.at ===
|
|
3125
|
+
'number' &&
|
|
3126
|
+
whitelistSync.at > 0
|
|
3127
|
+
? new Date(
|
|
3128
|
+
whitelistSync.at,
|
|
3129
|
+
).toLocaleString()
|
|
3130
|
+
: '—',
|
|
3131
|
+
}),
|
|
3132
|
+
invalid: false,
|
|
3133
|
+
}
|
|
3134
|
+
: {
|
|
3135
|
+
text: t('whitelistSyncError', {
|
|
3136
|
+
error:
|
|
3137
|
+
typeof whitelistSync.error ===
|
|
3138
|
+
'string' &&
|
|
3139
|
+
whitelistSync.error !== ''
|
|
3140
|
+
? whitelistSync.error
|
|
3141
|
+
: 'unknown',
|
|
3142
|
+
}),
|
|
3143
|
+
invalid: true,
|
|
3144
|
+
},
|
|
3145
|
+
]
|
|
3146
|
+
: [],
|
|
3147
|
+
}),
|
|
3148
|
+
),
|
|
3149
|
+
teamsMode
|
|
3150
|
+
? h('p', { style: STYLE.progress }, t('whitelistSyncNote'))
|
|
3151
|
+
: null,
|
|
3152
|
+
),
|
|
2336
3153
|
h(
|
|
2337
3154
|
'div',
|
|
2338
3155
|
{ style: STYLE.footer },
|
|
@@ -2398,6 +3215,19 @@ window.__ModuleLoader__.load({
|
|
|
2398
3215
|
apply(ctx) {
|
|
2399
3216
|
ctx.effect(() => ctx.locale.register(NS, DICTS));
|
|
2400
3217
|
const t = ctx.locale.bind(NS);
|
|
3218
|
+
// 报告当前生效的 UI 语言给 host 半场(决定语言询问指引的
|
|
3219
|
+
// 提问语言;fire-and-forget,失败静默——host 会退回
|
|
3220
|
+
// settings 的 locale.preference,再退回英文)。3 秒后补报
|
|
3221
|
+
// 一次,覆盖 host 路由晚于本页就绪的启动窗口。
|
|
3222
|
+
const reportUiLocale = () =>
|
|
3223
|
+
void api.post('ui-locale', {
|
|
3224
|
+
locale: ctx.locale?.getLocale?.().active ?? '',
|
|
3225
|
+
});
|
|
3226
|
+
reportUiLocale();
|
|
3227
|
+
setTimeout(reportUiLocale, 3000);
|
|
3228
|
+
// The session scope hands the badge the standard kit: the open
|
|
3229
|
+
// session's id and its live projection face (the model line),
|
|
3230
|
+
// plus the sessions service for the un-retained fallback.
|
|
2401
3231
|
ctx.slots.inject('conversation.session.header.actions', () =>
|
|
2402
3232
|
ctx.slots.register(
|
|
2403
3233
|
{
|
|
@@ -2405,7 +3235,60 @@ window.__ModuleLoader__.load({
|
|
|
2405
3235
|
id: 'dsh-switchman',
|
|
2406
3236
|
order: -9,
|
|
2407
3237
|
},
|
|
2408
|
-
() =>
|
|
3238
|
+
(kit) =>
|
|
3239
|
+
h(SwitchmanBadge, {
|
|
3240
|
+
...kit,
|
|
3241
|
+
locale: ctx.locale,
|
|
3242
|
+
t,
|
|
3243
|
+
}),
|
|
3244
|
+
),
|
|
3245
|
+
);
|
|
3246
|
+
// Home left-sidebar entry + central panel: the same slot pair
|
|
3247
|
+
// the shipped Skills Center row uses. The shell owns the row
|
|
3248
|
+
// button, click-to-switch, active highlight, and collapsed
|
|
3249
|
+
// tooltip. The sidebar id and the main key must match, and
|
|
3250
|
+
// the inject() face becomes the page component's props
|
|
3251
|
+
// ({t, locale} — SwitchmanPanelPage forwards them verbatim).
|
|
3252
|
+
// order 31 sits directly under the Skills Center row
|
|
3253
|
+
// (skill-explorer 30; plugins 0, schedules 10).
|
|
3254
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
3255
|
+
ctx.slots.register(
|
|
3256
|
+
{
|
|
3257
|
+
name: 'sidebar.panellist',
|
|
3258
|
+
id: 'dsh-switchman',
|
|
3259
|
+
order: 31,
|
|
3260
|
+
label: () => t('panelLabel'),
|
|
3261
|
+
locale: NS,
|
|
3262
|
+
},
|
|
3263
|
+
SwitchmanPanelIcon,
|
|
3264
|
+
),
|
|
3265
|
+
);
|
|
3266
|
+
ctx.slots.inject('main', () =>
|
|
3267
|
+
ctx.slots.register(
|
|
3268
|
+
{
|
|
3269
|
+
name: 'main',
|
|
3270
|
+
key: 'dsh-switchman',
|
|
3271
|
+
inject: () => ({ t, locale: ctx.locale }),
|
|
3272
|
+
},
|
|
3273
|
+
SwitchmanPanelPage,
|
|
3274
|
+
),
|
|
3275
|
+
);
|
|
3276
|
+
// Composer-adjacent vision entry (conversation.input.dock: the
|
|
3277
|
+
// full-width list ABOVE the composer card): guidance chip while
|
|
3278
|
+
// the current session's model cannot read images, plus reactive
|
|
3279
|
+
// auto-conversion of refused image sends into /vision submits.
|
|
3280
|
+
// The session scope automatically provides
|
|
3281
|
+
// inputActions/useInput/useSession to the registered
|
|
3282
|
+
// component; the wrapper forwards that standard kit through
|
|
3283
|
+
// alongside the closure-bound t.
|
|
3284
|
+
ctx.slots.inject('conversation.input.dock', () =>
|
|
3285
|
+
ctx.slots.register(
|
|
3286
|
+
{
|
|
3287
|
+
name: 'conversation.input.dock',
|
|
3288
|
+
id: 'dsh-switchman-vision-hint',
|
|
3289
|
+
order: 90,
|
|
3290
|
+
},
|
|
3291
|
+
(kit) => h(VisionDockEntry, { ...kit, t }),
|
|
2409
3292
|
),
|
|
2410
3293
|
);
|
|
2411
3294
|
// Settings page: one settings.section entry. The restricted
|