@morlay/dsh-session-mode 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,6 @@
1
- /**
2
- * composer 里的模式 chip:**本包在会话里唯一的面**(点开是 coding / chat 两项)。
3
- *
4
- * 选择只在会话开始**之前**有效——一旦跑过 turn,那段历史是在某个模式的工具与提示词下产生的。所以 chip 在
5
- * 会话里有两个形态:空白期是选择器,开过 turn 之后**只读**(只写当前模式,点不动)。判据是 host 的投影
6
- * `sessionModeEditable`(`turn/start` 一落库就为 `false`),与服务端拒绝切换读的是同一份事实。
7
- *
8
- * 它挂在 composer 上:新会话屏也是一个 blank session 的 composer,选择正好发生在那里;会话头部则没有它的
9
- * 位置——头部只在"跑起来之后"值得看,而那时的 chip 已经把当前模式写在脸上,再挂一个只读标签就是同一句话的
10
- * 复读。
11
- */
1
+ // composer 里的模式 chip:本包在会话里唯一的面(点开是 coding / chat 两项)。
2
+ // 选择只在会话开始**之前**有效(那段历史属于某个模式的工具与提示词),所以 chip 有两个形态:空白期是选择器,
3
+ // 开过 turn 之后只读。判据是 host 的投影 `sessionModeEditable`,与服务端拒绝切换读的是同一份事实。
12
4
 
13
5
  import { useState } from "react";
14
6
  import {
@@ -25,20 +17,12 @@ import { selectMode } from "./api.ts";
25
17
  import { useRoster } from "./use-roster.ts";
26
18
  import css from "./SessionModeSeat.module.css";
27
19
 
28
- /** 完整 props:composer 工具行左侧槽位的运行时 props + 本包的字典。 */
20
+ // 完整 props:composer 工具行左侧槽位的运行时 props + 本包的字典。
29
21
  export type SessionModeSeatProps = PropsRuntime<"conversation.input.left"> &
30
22
  PropsLocale<"session-mode">;
31
23
 
32
- /**
33
- * 渲染 composer 里的模式切换 chip。
34
- *
35
- * 为什么在 composer(`conversation.input.left`):**新会话屏也是一个 blank session 的 composer**,选择正好
36
- * 发生在那里;会话头部槽位在新会话屏根本不存在,而会话跑起来之后头部也不该重复说一遍 chip 已经写着的事。
37
- * 官方 roster 那个座位(`conversation.hero.agentPreset`)是单注册槽位、已被官方占了,所以用自己的 list 槽位
38
- * 并存。
39
- * @param props - 槽位合成的 props。
40
- * @returns chip;清单没读到、或会话未知时返回 null。
41
- */
24
+ // 渲染 composer 里的模式切换 chip:新会话屏也是 blank session 的 composer,选择正好发生在那里;官方 roster 占了
25
+ // 单注册槽位 `conversation.hero.agentPreset`,所以这里用自己的 list 槽位并存。清单没读到时会话未知时返回 null。
42
26
  export function SessionModeSeat({ sessionId, useSessions, t }: SessionModeSeatProps) {
43
27
  const roster = useRoster();
44
28
  const selected = useSessions((state) => {
@@ -63,8 +47,7 @@ export function SessionModeSeat({ sessionId, useSessions, t }: SessionModeSeatPr
63
47
  const chosen = roster.modes.find((mode) => mode.id === current);
64
48
  const label = chosen?.name ?? current;
65
49
 
66
- // 开过 turn 的会话**只读**:chip 只写当前模式,点不动也不给清单。判据与 host 拒绝切换读的是同一个投影
67
- // (`sessionModeEditable`),所以不会出现"看起来能选、点了报错"。
50
+ // 开过 turn 的会话只读:chip 只写当前模式、点不动(判据与 host 拒绝切换读的是同一个投影)。
68
51
  if (editable === false) {
69
52
  return (
70
53
  <button type="button" className={css.seat} disabled title={t("lockedHint")}>
package/src/client/api.ts CHANGED
@@ -1,9 +1,5 @@
1
- /**
2
- * client 半与 host 的**唯一**通路:模式清单(`GET`)与切换(`POST`)。
3
- *
4
- * 当前模式不走这里——它是一条会话投影(`sessionMode`),随会话列表一起到页面,组件用 `useSessions`
5
- * 读它。于是这里只剩两件事:拉清单、发一次切换。
6
- */
1
+ // client 半与 host 的**唯一**通路:模式清单(`GET`)与切换(`POST`)。当前模式是会话投影(`sessionMode`),
2
+ // 随会话列表一起到页面,不走这里。
7
3
 
8
4
  import {
9
5
  SESSION_MODE_PATH,
@@ -11,7 +7,7 @@ import {
11
7
  type SessionModeSelectResult,
12
8
  } from "../shared.ts";
13
9
 
14
- /** 读一次模式清单;清单在页面生命周期里是静态的(装配事实),调用方自己缓存。 */
10
+ // 读一次模式清单;清单在页面生命周期里是静态的(装配事实),调用方自己缓存。
15
11
  export async function fetchRoster(): Promise<SessionModeRoster> {
16
12
  const response = await fetch(SESSION_MODE_PATH, {
17
13
  method: "GET",
@@ -21,13 +17,7 @@ export async function fetchRoster(): Promise<SessionModeRoster> {
21
17
  return (await response.json()) as SessionModeRoster;
22
18
  }
23
19
 
24
- /**
25
- * 把某个空白会话切到某个模式。
26
- * @param sessionId - 目标会话。
27
- * @param mode - 目标模式 id。
28
- * @returns 提交后的模式 id。
29
- * @throws 会话已开始、模式不存在、会话不存在——host 给的中文原因。
30
- */
20
+ // 把某个空白会话切到某个模式;失败抛 host 给的中文原因(会话已开始 / 模式不存在 / 会话不存在)。
31
21
  export async function selectMode(sessionId: string, mode: string): Promise<string> {
32
22
  const response = await fetch(SESSION_MODE_PATH, {
33
23
  method: "POST",
@@ -1,49 +1,40 @@
1
- /**
2
- * client 半:会话里那一个面(模式 chip)+ 设置页里那张「会话模式」卡片。
3
- *
4
- * | 槽位 | 呈现 |
5
- * | ------------------------- | --------------------------------------------------- |
6
- * | `conversation.input.left` | chip——点开就是切换列表(新会话屏也是一个 composer) |
7
- *
8
- * 官方 `@deepseek-ai/dsh-client-ui-agent-preset` 保留(它提供 preset 的选择面与设置页 roster):官方管"挂哪套
9
- * 行",我们管"会话级扩展",两套入口并存、互不遮挡。
10
- *
11
- * 头部的只读标签**没有**:chip 本来就把当前模式写在脸上(会话与新会话屏都有 composer),右上角再写一遍是
12
- * 同一句话的复读。
13
- *
14
- * 本行的配置页(各模式的默认模型 `config.models`)不再由本包画:那个字段是 volatile 的,页面由
15
- * `@morlay/dsh-client-ui-primitives` 按 schema 自动生成(它注册到本行的配置入口 `plugins.row.config`,
16
- * key = `<bundle 包名>#<行 id>`)。本包只管会话里那一个面。
17
- */
1
+ // client 半:会话里那一个面(模式 chip,槽位 `conversation.input.left`,list + session scope)+ 配置页字段文案。
2
+ // 官方 `@deepseek-ai/dsh-client-ui-agent-preset` 保留(官方管"挂哪套行",我们管"会话级扩展",两套入口并存);
3
+ // 本行的配置页由 `@morlay/dsh-client-ui-primitives` 按 schema 自动生成,key = `<bundle 包名>#<行 id>`。
18
4
 
19
5
  import type { Context } from "@deepseek-ai/cordis";
20
6
  // Type-only:`ctx.remote` 的合并面(选模型的候选来自 LLM 目录)。
21
7
  import type {} from "@deepseek-ai/dsh-api-remotes/client";
22
8
  import type {} from "@deepseek-ai/dsh-client-locale/client";
23
9
  import type {} from "@deepseek-ai/dsh-client-ui-slots";
24
-
25
10
  // Type-only:槽位声明与 standard props(session / session-maybe / global)。
26
11
  import type {} from "@morlay/dsh-client-ui-conversation/client";
12
+ import { POLICY_NAMES } from "../shared.ts";
27
13
  import { SessionModeSeat } from "./SessionModeSeat.tsx";
28
14
  import { en, zh, type SessionModeLocaleKey } from "./locales.ts";
29
15
 
30
16
  declare module "@deepseek-ai/dsh-client-ui-slots" {
31
17
  interface LocaleNamespaceMap {
32
- /** 会话里两个面的文案(配置页文案在通用 schema 表单的字典里)。 */
18
+ // 会话里两个面的文案(配置页文案在通用 schema 表单的字典里)。
33
19
  "session-mode": SessionModeLocaleKey;
34
20
  }
35
21
  }
36
22
 
37
- /** 浏览器半插件的字典命名空间。 */
23
+ // 浏览器半插件的字典命名空间。
38
24
  const NS = "session-mode";
39
25
 
40
- /** 这一行里需要中文文案的字段(都在 `modes.<模式>.defaultModel` 里,所以按模板路径注册一次)。 */
26
+ // 这一行里需要中文文案的字段(都在 `modes.<模式>.defaultModel` 里,所以按模板路径注册一次)。
41
27
  const FIELDS = ["provider", "model", "reasoningEffort"] as const;
42
28
 
43
- /** 动态键的占位段(与通用表单的字段树同一约定)。 */
29
+ // 两份 policy 名单的字段名(`modes.<模式>.<字段>`):它们在 schema 上是**字符串数组**(`z.array(z.string())`,没有
30
+ // enum),候选值由本文件登记的封闭名单 `POLICY_NAMES` 给(见下面 `hints.select` 那一段)——所以两条已知 policy
31
+ // 都以"未配置"的形态各占一项可选,页面把每一项画成选择器。
32
+ const POLICY_FIELDS = ["allowPolicies", "denyPolicies"] as const;
33
+
34
+ // 动态键的占位段(与通用表单的字段树同一约定)。
44
35
  const DYNAMIC = "*";
45
36
 
46
- /** 一个可配置 provider 的候选信息:显示名 + 它的配置在哪(模型清单从那份配置里读)。 */
37
+ // 一个可配置 provider 的候选信息:显示名 + 它的配置在哪(模型清单从那份配置里读)。
47
38
  interface ProviderEntry {
48
39
  value: string;
49
40
  label: string;
@@ -54,13 +45,13 @@ interface ProviderEntry {
54
45
  export type { SessionModeSeatProps } from "./SessionModeSeat.tsx";
55
46
  export type { SessionModeLocaleKey } from "./locales.ts";
56
47
 
57
- /** 需要的服务:槽位与字典(会话列表经槽位的标准 props 到达组件,不必自己 inject)。 */
48
+ // 需要的服务:槽位与字典(会话列表经槽位的标准 props 到达组件,不必自己 inject)。
58
49
  export const inject = ["slots", "locale"];
59
50
 
60
- /** 本包 host 行 id:行配置页读的就是这个命名空间。 */
51
+ // 本包 host 行 id:行配置页读的就是这个命名空间。
61
52
  export const SESSION_MODE_NS = "session-mode";
62
53
 
63
- /** 按路径读一段配置里的值(本包只读 provider 档案里的模型清单)。 */
54
+ // 按路径读一段配置里的值(本包只读 provider 档案里的模型清单)。
64
55
  function readAt(root: unknown, path: readonly string[]): unknown {
65
56
  return path.reduce<unknown>((node, segment) => {
66
57
  if (Array.isArray(node)) return node[Number(segment)];
@@ -69,11 +60,7 @@ function readAt(root: unknown, path: readonly string[]): unknown {
69
60
  }, root);
70
61
  }
71
62
 
72
- /**
73
- * 装上会话里的两个面,以及本行配置页的字段文案。
74
- *
75
- * @param ctx - 浏览器插件上下文。
76
- */
63
+ // 装上会话里的那一个面,以及本行配置页的字段文案。
77
64
  export function apply(ctx: Context): void {
78
65
  const t = ctx.locale.bind(NS);
79
66
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-mode: dictionaries");
@@ -95,6 +82,22 @@ export function apply(ctx: Context): void {
95
82
  }, "session-mode: field wording"),
96
83
  );
97
84
 
85
+ // 两份 policy 名单是**字符串数组**,键的候选是一份封闭名单(上游 waterfall 名,见 `shared.ts` 的
86
+ // `POLICY_NAMES`):在数组**项**那一路径上登记候选值,页面因此把每一项画成选择器,两条已知 policy 直接可选
87
+ // (不必手写名字)。名单住在 `shared.ts`——client 半不 import `modes.ts`(免得把 schemastery 拖进浏览器包)。
88
+ ctx.inject(["schemaFormHints"], (scope) =>
89
+ scope.effect(() => {
90
+ const offs = POLICY_FIELDS.map((key) =>
91
+ scope.schemaFormHints.select(SESSION_MODE_NS, ["modes", DYNAMIC, key, DYNAMIC], {
92
+ options: () => POLICY_NAMES.map((value) => ({ value })),
93
+ }),
94
+ );
95
+ return () => {
96
+ for (const off of offs) off();
97
+ };
98
+ }, "session-mode: policy candidates"),
99
+ );
100
+
98
101
  // 选模型的候选不在本行的 schema 里:provider 是部署里的 LLM 目录(活着的路由 + 可配置声明),模型清单读那份声明
99
102
  // 指向的配置(`settingsNs` / `settingsPath`)。目录异步取到后注册——注册本身就是一次变更通知。
100
103
  ctx.inject(["schemaFormHints"], (scope) =>
@@ -1,9 +1,5 @@
1
- /**
2
- * client 半的文案:命名空间 `session-mode`(与 locale 注册时用的 key 一致)。
3
- *
4
- * 只有会话里那个模式 chip 用这份字典:模式的名字与说明由 host 的清单给(那是数据,不是文案)。本行的配置页
5
- * (各模式的默认模型)由通用 schema 表单渲染,文案在它自己的字典里。
6
- */
1
+ // client 半的文案:命名空间 `session-mode`(与 locale 注册时用的 key 一致)。模式的名字与说明由 host 的清单给
2
+ // (那是数据,不是文案);本行配置页的文案在通用 schema 表单自己的字典里。
7
3
 
8
4
  export const zh = {
9
5
  seatHint: "选择这个会话的模式",
@@ -2,13 +2,11 @@ import { useEffect, useState } from "react";
2
2
  import type { SessionModeRoster } from "../shared.ts";
3
3
  import { fetchRoster } from "./api.ts";
4
4
 
5
- /**
6
- * 模块级缓存:清单是装配事实(`session-mode` 行的 config),页面生命周期里不会变,两次挂载之间不必重拉。
7
- * 失败不缓存——下次挂载就是一次重试。
8
- */
5
+ // 模块级缓存:清单是装配事实(`session-mode` 行的 config),页面生命周期里不会变,两次挂载之间不必重拉。
6
+ // 失败不缓存——下次挂载就是一次重试。
9
7
  let pending: Promise<SessionModeRoster> | undefined;
10
8
 
11
- /** 读模式清单;还没读到(或读失败)时返回 undefined,组件据此不渲染。 */
9
+ // 读模式清单;还没读到(或读失败)时返回 undefined,组件据此不渲染。
12
10
  export function useRoster(): SessionModeRoster | undefined {
13
11
  const [roster, setRoster] = useState<SessionModeRoster | undefined>(undefined);
14
12
  useEffect(() => {