@khorsheed/dsh-client-session-title-edit 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.2.0(2026-09-10)
4
+
5
+ 适配宿主 0.1.2 线。
6
+
7
+ - **BREAKING**:minHost 前移至 `0.1.2-rc.1`;宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 的用户请停留在 0.1.x 线(末版 `0.1.0`)
8
+ - 重命名失败错误码双兼容:同时识别 0.1.2 命名空间化的 `session/title-invalid` 与旧的 `title-invalid`
9
+ - 导入面迁移:client bundle 不再引用宿主已删除的 `dsh-client-runtime`
10
+
3
11
  ## 0.1.0(2026-08-22)
4
12
 
5
13
  首个公开发布。
package/README.en.md CHANGED
@@ -1,12 +1,12 @@
1
- # @khorsheed/dsh-client-session-title-edit
1
+ # dsh-client-session-title-edit
2
2
 
3
3
  English | [中文](README.md)
4
4
 
5
- Rename a session right in the dsh web GUI chat header: click the pencil next to the title and the title itself becomes an inline editor. Enter commits, Escape cancels, over-long drafts are blocked with a localized warning — and the model never sees any of it.
5
+ Rename a session whenever you like: click the little pencil by the title, hit Enter, done.
6
6
 
7
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit1.png" width="480" alt="inline session title editor in the chat header">
7
+ Auto-generated session titles are often off the mark, and finding a session again two days later is pure luck. This plugin puts a pencil next to the title in the chat header: one click and the title becomes an input in place, prefilled and fully selected. Enter saves, Escape cancels, and a draft that's too long gets a warning instead of silent truncation. Renames go through the official `session.rename` RPC, and the title never enters the model context — the model knows nothing about it.
8
8
 
9
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit2.png" width="480" alt="click the pencil and the title becomes an input — Enter saves">
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit1.png" width="640" alt="the pencil button beside the title in the chat header, with a rename-session tooltip on hover">
10
10
 
11
11
  ## Features
12
12
 
@@ -16,6 +16,8 @@ Rename a session right in the dsh web GUI chat header: click the pencil next to
16
16
  - **Budget-aware** — drafts past the host's 80-UTF-8-byte title budget are blocked with a warning, never silently truncated.
17
17
  - **Zero footprint** — rides the official `session.rename` RPC; the title never enters model context (no token or KV-cache effect).
18
18
 
19
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit2.png" width="640" alt="after clicking the pencil the title becomes an input in place — edit and press Enter to save">
20
+
19
21
  ## Install
20
22
 
21
23
  ```sh
@@ -30,8 +32,10 @@ dsh plugin --profile web remove @khorsheed/dsh-client-session-title-edit
30
32
 
31
33
  ## Compatibility
32
34
 
33
- - npm release line (`@deepseek-ai/dsh@0.1.1-rc.2`): ✅ full — the rc.8→0.1.1-rc.1 API audit (2026-08-21) confirms every surface this plugin consumes is unchanged or additive (the ProjectionDefinition restructure, cacheHitPercent return-type change, and the credentials/updated event rename do not touch this package); no source change was needed; re-audited for rc.2 (2026-08-22): consumed surface unchanged, full build+test green.
34
- - source line (deepseek-harness master): ✅
35
+ - npm release line (`@deepseek-ai/dsh@0.1.2-rc.1`): ✅ full — baseline moved to the 0.1.2-rc.1 API surface (single-arm 0.1.2 API consumption; the 0.1.1-rc.2 runtime arm is retired), full build+test green; minHost moves up to 0.1.2-rc.1 — older hosts stay on the previous release line.
36
+ - source line (deepseek-harness master): ✅ (verifiedHost: 0.1.2-rc.1)
37
+
38
+ **Version line mapping**: 0.2.0 and up support host `0.1.2-rc.1` and later; hosts on `0.1.0-rc.6` ~ `0.1.1-rc.2` stay on the 0.1.x release line (last release `0.1.0`).
35
39
 
36
40
  ## Known Limitations
37
41
 
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # last confirmed-consistent state. Both languages carry equal authority; after
3
3
  # editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/session-title-edit/README.en.md
5
- packages/session-title-edit/README.en.md: 9b0bc61b534c9b2221996afbb1f0da10f011526d
6
- packages/session-title-edit/README.md: 1117e60c88c6ca7ec99dfb659d59ae98e1287865
5
+ packages/session-title-edit/README.en.md: cbbe2ec82fea49b19470530a0320c4f8ce430bca
6
+ packages/session-title-edit/README.md: 824eeb723447f7fe5b0337f43d903e39dcb7fbff
package/README.md CHANGED
@@ -2,19 +2,21 @@
2
2
 
3
3
  [English](README.en.md) | 中文
4
4
 
5
- 在 dsh web GUI 聊天区头部直接重命名会话:点击标题旁的铅笔,标题本身变成内联编辑器。Enter 提交、Escape 取消,超长草稿会被本地化警告拦下——模型对此完全无感。
5
+ 会话标题想改就改:点标题旁的小铅笔,回车就存。
6
6
 
7
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit1.png" width="480" alt="聊天头部的内联会话标题编辑器">
7
+ 自动起的会话标题常常词不达意,过两天想找回某个会话全靠运气。这个插件在聊天区顶部的标题旁放了一支铅笔:点一下,标题就地变成输入框,预填好当前名字并全选;Enter 保存、Escape 取消,名字起得太长会先被警告拦下,绝不悄悄截断。改名走的是官方 `session.rename` 接口,标题永远不会进入模型上下文,模型对此一无所知。
8
8
 
9
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit2.png" width="480" alt="点击铅笔后标题变成输入框,回车即保存">
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit1.png" width="640" alt="聊天区头部标题旁的铅笔按钮,悬停显示「重命名会话」">
10
10
 
11
11
  ## 特性
12
12
 
13
- - **头部铅笔**——会话标题右侧的条目切换出内联编辑器,预填当前标题并全选。
13
+ - **头部铅笔**——会话标题右侧的条目切换出内联编辑器,预填当前标题并全选。
14
14
  - **可预期的按键**——Enter 提交、Escape 取消、空草稿禁用保存、宿主拒绝时内联提示。
15
- - **宽度自适应**——输入框随草稿变宽,上限为官方 220px。
16
- - **预算把关**——超过宿主 80 UTF-8 字节标题预算的草稿被警告拦下,绝不静默截断。
17
- - **零足迹**——走官方 `session.rename` RPC;标题永远不会进入模型上下文(无 token 或 KV 缓存影响)。
15
+ - **宽度自适应**——输入框随草稿变宽,上限为官方 220px。
16
+ - **预算把关**——超过宿主 80 UTF-8 字节标题预算的草稿被警告拦下,绝不静默截断。
17
+ - **零足迹**——走官方 `session.rename` RPC;标题永远不会进入模型上下文(无 token 或 KV 缓存影响)。
18
+
19
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/session-title-edit2.png" width="640" alt="点击铅笔后标题就地变成输入框,直接修改后回车保存">
18
20
 
19
21
  ## 安装
20
22
 
@@ -22,7 +24,7 @@
22
24
  dsh plugin --profile web add @khorsheed/dsh-client-session-title-edit
23
25
  ```
24
26
 
25
- 安装后重启 web 实例;卸载即移除本插件添加的全部界面。
27
+ 安装后重启 web 实例;卸载即移除本插件添加的全部界面。
26
28
 
27
29
  ```sh
28
30
  dsh plugin --profile web remove @khorsheed/dsh-client-session-title-edit
@@ -30,40 +32,42 @@ dsh plugin --profile web remove @khorsheed/dsh-client-session-title-edit
30
32
 
31
33
  ## Compatibility
32
34
 
33
- - npm 发布线(`@deepseek-ai/dsh@0.1.1-rc.2`):✅ 完整——rc.8→0.1.1-rc.1 API 审计(2026-08-21)确认本插件消费的所有面无变化或纯增量(ProjectionDefinition 重构、cacheHitPercent 返回值变更、credentials/updated 事件改名均不涉及本包),无需改动源码;rc.1→rc.2 复核(2026-08-22):消费面无变化,全量构建测试通过。
34
- - 源码线(deepseek-harness master):✅
35
+ - npm 发布线(`@deepseek-ai/dsh@0.1.2-rc.1`):✅ 完整——基线迁移至 0.1.2-rc.1 API 面(单臂消费 0.1.2 API,0.1.1-rc.2 运行臂已退役),全量构建测试通过;minHost 前移至 0.1.2-rc.1,旧宿主请停留在旧发布线。
36
+ - 源码线(deepseek-harness master):✅(verifiedHost: 0.1.2-rc.1)
37
+
38
+ **版本线对照**:0.2.0 起支持宿主 `0.1.2-rc.1` 及以后;宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 的用户请停留在 0.1.x 发布线(末版 `0.1.0`)。
35
39
 
36
40
  ## 已知限制
37
41
 
38
- - **原位编辑是 DOM 层过渡方案**——官方 header 没有暴露标题槽位,编辑器通过隐藏官方标题 crumb 并原位覆盖实现;官方 DOM 变化时退回 actions 行内编辑器。
42
+ - **原位编辑是 DOM 层过渡方案**——官方 header 没有暴露标题槽位,编辑器通过隐藏官方标题 crumb 并原位覆盖实现;官方 DOM 变化时退回 actions 行内编辑器。
39
43
  - **无乐观更新**——header 标题只在宿主投影结算重命名后刷新。
40
- - **字节预算归宿主所有,由客户端把关**——客户端镜像宿主的 80 字节上限;宿主提高上限后,需常量同步跟进编辑器才会放宽。
44
+ - **字节预算归宿主所有,由客户端把关**——客户端镜像宿主的 80 字节上限;宿主提高上限后,需常量同步跟进编辑器才会放宽。
41
45
 
42
46
  ## 实现原理
43
47
 
44
48
  <details>
45
- <summary>内部结构(点击展开)</summary>
49
+ <summary>内部结构(点击展开)</summary>
46
50
 
47
51
  ```
48
- src/index.ts 导出(apply/inject + TitleEditActionProps)
49
- src/client/index.ts 插件主体;贡献 header-actions 条目
52
+ src/index.ts 导出(apply/inject + TitleEditActionProps)
53
+ src/client/index.ts 插件主体;贡献 header-actions 条目
50
54
  src/client/TitleEditAction.tsx 铅笔条目、内联编辑器、覆盖与宽度适配
51
55
  src/client/title-length.ts MAX_TITLE_BYTES 镜像 + 宿主归一化
52
56
  src/client/locales.ts 本地化警告与错误
53
57
  src/client/slots.ts 槽位声明
54
58
  ```
55
59
 
56
- 纯浏览器侧插件:重命名走官方 `session.rename` RPC(`session.rename` → `sessions.rename` → `ctx.sessionTitle.rename`),因此无需宿主半边、无需新增 RPC、无需改动任何官方包。
60
+ 纯浏览器侧插件:重命名走官方 `session.rename` RPC(`session.rename` → `sessions.rename` → `ctx.sessionTitle.rename`),因此无需宿主半边、无需新增 RPC、无需改动任何官方包。
57
61
 
58
- 编辑是原位的:点击铅笔后,通过 `data-ste-inplace` 隐藏官方标题 crumb,并在其测量矩形上覆盖本插件的输入框。输入框宽度借助隐藏的、与输入框同字体的镜像 span 按草稿适配,下限为 crumb 原宽度、上限为 crumb 的 220px 上限。
62
+ 编辑是原位的:点击铅笔后,通过 `data-ste-inplace` 隐藏官方标题 crumb,并在其测量矩形上覆盖本插件的输入框。输入框宽度借助隐藏的、与输入框同字体的镜像 span 按草稿适配,下限为 crumb 原宽度、上限为 crumb 的 220px 上限。
59
63
 
60
- 对模型没有任何变化:标题是仅投影的会话属性,永不进入模型上下文;重命名只是追加一条带用户来源的 `session/title` 事件——无 token 或 KV 缓存影响,投影落地后 header 标题随之刷新。另外,`@deepseek-ai/dsh-session-title` 限制接受的标题长度(`maxTitleBytes`,生产默认 80 UTF-8 字节);编辑器镜像该上限以及宿主截断前的归一化(剥离转义/控制/方向序列、折叠空白、去首尾空白),把关恰好在宿主会截断时触发。
64
+ 对模型没有任何变化:标题是仅投影的会话属性,永不进入模型上下文;重命名只是追加一条带用户来源的 `session/title` 事件——无 token 或 KV 缓存影响,投影落地后 header 标题随之刷新。另外,`@deepseek-ai/dsh-session-title` 限制接受的标题长度(`maxTitleBytes`,生产默认 80 UTF-8 字节);编辑器镜像该上限以及宿主截断前的归一化(剥离转义/控制/方向序列、折叠空白、去首尾空白),把关恰好在宿主会截断时触发。
61
65
 
62
66
  </details>
63
67
 
64
68
  ## 开发
65
69
 
66
- 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/session-title-edit`)。问题与贡献请移步该仓库。
70
+ 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/session-title-edit`)。问题与贡献请移步该仓库。
67
71
 
68
72
  ## 变更记录
69
73
 
package/lib/client.js CHANGED
@@ -92,8 +92,8 @@ window.__ModuleLoader__.load({
92
92
  return utf8ByteLength(input.replace(OSC_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESC_SEQUENCE, "").replace(CONTROL_CHARACTER, "").replace(DIRECTIONAL_CONTROL, "").replace(/\s+/gu, " ").trim());
93
93
  }
94
94
  //#endregion
95
- //#region \0dsh-css:/Users/zhuyudan/code/dsh-plugins/packages/session-title-edit/src/client/TitleEditAction.module.css.mjs
96
- const css = ".okd51a_renameButton{width:22px;height:22px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;margin-left:-8px;padding:0;display:inline-flex}.okd51a_renameButton:hover,.okd51a_renameButton:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.okd51a_editor{align-items:center;gap:6px;display:inline-flex}.okd51a_editorInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:220px;height:28px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:6px;outline:none;padding:3px 8px;font-size:12px;line-height:18px}.okd51a_editorInput:focus{border-color:var(--dsw-alias-brand-primary)}.okd51a_editorInput:disabled{color:var(--dsw-alias-label-dimmed)}.okd51a_editorError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}.okd51a_editorHint{color:var(--dsw-alias-state-warn-primary);font-size:12px;line-height:18px}.okd51a_inPlaceInput{box-sizing:border-box;z-index:40;color:var(--dsw-alias-label-primary);background:0 0;border:1px solid #0000;border-radius:12px;outline:none;padding:4px 8px;font-size:14px;font-weight:500;line-height:20px;position:fixed}.okd51a_inPlaceInput:focus{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-base)}.okd51a_inPlaceInput:disabled{color:var(--dsw-alias-label-dimmed)}.okd51a_inPlaceMirror{visibility:hidden;white-space:pre;pointer-events:none;font-family:inherit;font-size:14px;font-weight:500;position:absolute;top:-9999px;left:-9999px}.okd51a_inPlaceError{color:var(--dsw-alias-state-error-primary);margin-left:6px;font-size:12px;line-height:18px}.okd51a_inPlaceHint{color:var(--dsw-alias-state-warn-primary);margin-left:6px;font-size:12px;line-height:18px}header nav button[data-ste-inplace]{visibility:hidden!important}";
95
+ //#region \0dsh-css:src/client/TitleEditAction.module.css.mjs
96
+ const css = ".okd51a_renameButton{width:22px;height:22px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;margin-left:-12px;padding:0;display:inline-flex}.okd51a_renameButton:hover,.okd51a_renameButton:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.okd51a_editor{align-items:center;gap:6px;display:inline-flex}.okd51a_editorInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:220px;height:28px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:6px;outline:none;padding:3px 8px;font-size:12px;line-height:18px}.okd51a_editorInput:focus{border-color:var(--dsw-alias-brand-primary)}.okd51a_editorInput:disabled{color:var(--dsw-alias-label-dimmed)}.okd51a_editorError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}.okd51a_editorHint{color:var(--dsw-alias-state-warn-primary);font-size:12px;line-height:18px}.okd51a_inPlaceInput{box-sizing:border-box;z-index:40;color:var(--dsw-alias-label-primary);background:0 0;border:1px solid #0000;border-radius:12px;outline:none;padding:4px 8px;font-size:14px;font-weight:500;line-height:20px;position:fixed}.okd51a_inPlaceInput:focus{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-base)}.okd51a_inPlaceInput:disabled{color:var(--dsw-alias-label-dimmed)}.okd51a_inPlaceMirror{visibility:hidden;white-space:pre;pointer-events:none;font-family:inherit;font-size:14px;font-weight:500;position:absolute;top:-9999px;left:-9999px}.okd51a_inPlaceError{color:var(--dsw-alias-state-error-primary);margin-left:6px;font-size:12px;line-height:18px}.okd51a_inPlaceHint{color:var(--dsw-alias-state-warn-primary);margin-left:6px;font-size:12px;line-height:18px}header nav button[data-ste-inplace]{visibility:hidden!important}";
97
97
  const tagId = "@khorsheed/dsh-client-session-title-edit/TitleEditAction.module.css";
98
98
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
99
99
  const tag = document.createElement("style");
@@ -103,15 +103,15 @@ window.__ModuleLoader__.load({
103
103
  document.head.appendChild(tag);
104
104
  }
105
105
  var TitleEditAction_module_css_default = {
106
- "editorInput": "okd51a_editorInput",
107
106
  "renameButton": "okd51a_renameButton",
108
- "inPlaceError": "okd51a_inPlaceError",
109
- "editor": "okd51a_editor",
107
+ "inPlaceInput": "okd51a_inPlaceInput",
110
108
  "editorError": "okd51a_editorError",
109
+ "inPlaceError": "okd51a_inPlaceError",
110
+ "editorInput": "okd51a_editorInput",
111
111
  "editorHint": "okd51a_editorHint",
112
- "inPlaceMirror": "okd51a_inPlaceMirror",
113
112
  "inPlaceHint": "okd51a_inPlaceHint",
114
- "inPlaceInput": "okd51a_inPlaceInput"
113
+ "inPlaceMirror": "okd51a_inPlaceMirror",
114
+ "editor": "okd51a_editor"
115
115
  };
116
116
  //#endregion
117
117
  //#region src/client/TitleEditAction.tsx
@@ -257,7 +257,7 @@ window.__ModuleLoader__.load({
257
257
  await renameSession(title);
258
258
  closeEditor();
259
259
  } catch (cause) {
260
- setError(isRenameFailure(cause) && cause.code === "title-invalid" ? t("error.invalid") : t("error.rename"));
260
+ setError(isRenameFailure(cause) && (cause.code === "session/title-invalid" || cause.code === "title-invalid") ? t("error.invalid") : t("error.rename"));
261
261
  } finally {
262
262
  setBusy(false);
263
263
  }
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState","useRef","css","IconEditOutline16","Button"],"sources":["../src/client/locales.ts","../src/client/slots.ts","../src/client/title-length.ts","../src/client/TitleEditAction.tsx","../src/client/index.ts"],"sourcesContent":["/** `session-title-edit` namespace dictionaries. */\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'action.rename': '重命名会话',\n 'editor.aria': '会话标题',\n 'editor.placeholder': '输入新标题…',\n 'editor.save': '保存',\n 'cancel': '取消',\n 'error.invalid': '标题不能为空',\n 'error.rename': '重命名失败,请重试',\n 'hint.tooLong': '标题过长:最多 {max} 字节(约 {chars} 个汉字),当前 {bytes} 字节',\n} satisfies Record<string, string>\n\n/** The session-title-edit namespace key union. */\nexport type SessionTitleEditKey = keyof typeof zh\n\n/** The dictionary namespace owned by this plugin. */\nexport const NS = 'session-title-edit'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The session-title editing control's copy. */\n 'session-title-edit': SessionTitleEditKey\n }\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'action.rename': 'Rename session',\n 'editor.aria': 'Session title',\n 'editor.placeholder': 'Enter a new title…',\n 'editor.save': 'Save',\n 'cancel': 'Cancel',\n 'error.invalid': 'The title cannot be empty',\n 'error.rename': 'Rename failed, please try again',\n 'hint.tooLong': 'Title too long: max {max} bytes (about {chars} CJK characters), currently {bytes}',\n} satisfies Record<string, string>\n","/**\n * Slot-facing types of the session-title-edit client half: the injected\n * rename action face and the composed props of its one\n * `conversation.session.header.actions` entry.\n */\nimport type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: pulls ui-conversation's SlotMap merge\n// ('conversation.session.header.actions').\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: pulls this plugin's LocaleNamespaceMap merge.\nimport type {} from './locales.ts'\n\n/** Rename failure carrying the wire error code for localized surfacing. */\nexport interface RenameFailure extends Error {\n /** Wire error code (e.g. `title-invalid`); transport failures keep the code the runtime reported. */\n code: string\n}\n\n/**\n * Narrow an unknown thrown value to a {@link RenameFailure}.\n * @param error - the value caught from a rename attempt.\n * @returns true when the value is an Error carrying a string `code`.\n */\nexport function isRenameFailure(error: unknown): error is RenameFailure {\n return error instanceof Error && typeof (error as RenameFailure).code === 'string'\n}\n\n/** Injected action face of the header entry. */\nexport interface SessionTitleEditInjected {\n /**\n * Rename the current session through the official session.rename RPC. The\n * accepted user title pins the session against automatic regeneration.\n * @param title - raw user input; the host normalizes and caps it.\n * @returns when the host accepted the title.\n * @throws {RenameFailure} carrying the wire error code on host rejection.\n */\n renameSession: (title: string) => Promise<void>\n}\n\n/** Full props of the session-header title-edit entry. */\nexport type TitleEditActionProps =\n PropsRuntime<'conversation.session.header.actions'>\n & InjectFace<SessionTitleEditInjected>\n & PropsLocale<'session-title-edit'>\n","/**\n * Client-side mirror of the host session-title byte budget and the\n * normalization the host applies before capping, so the editor can gate\n * against the exact title the host would accept instead of discovering a\n * silent truncation after commit.\n *\n * The host contract: `@deepseek-ai/dsh-session-title`'s\n * `normalizeSessionTitle(input, maxTitleBytes)` cleans the text (ESC/CSI/\n * control/directional sequences stripped, whitespace collapsed to one space,\n * trimmed) and truncates to a UTF-8 byte budget. The production default is\n * `maxTitleBytes: 80` (deepseek-harness `packages/bundle/base/cordis.patch.yml`).\n * The clean step and the constant below mirror that behavior; update them\n * together if the host changes either.\n */\n\n/** Strip operating-system-command escape sequences, including unterminated tails. */\nconst OSC_SEQUENCE = /(?:\\u001B\\]|\\u009D)(?:(?!\\u0007|\\u001B\\\\)[\\s\\S])*(?:\\u0007|\\u001B\\\\|$)/gu\n/** Strip control-sequence-introducer escapes such as SGR color codes. */\nconst CSI_SEQUENCE = /(?:\\u001B\\[|\\u009B)[0-?]*[ -/]*[@-~]/gu\n/** Strip remaining two-byte ESC control sequences. */\nconst ESC_SEQUENCE = /\\u001B[@-_]/gu\n/** Strip non-whitespace C0/C1 control characters. */\nconst CONTROL_CHARACTER = /[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u009F]/gu\n/** Strip directional and invisible controls that can make a displayed title deceptive. */\nconst DIRECTIONAL_CONTROL = /[\\u200B\\u200E\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF]/gu\n\n/**\n * Host cap on any accepted session title, in UTF-8 bytes. Mirrors the\n * production default `maxTitleBytes: 80`; keep in sync with the host config.\n * 80 bytes ≈ 80 ASCII characters or ≈ 26 CJK characters (3 bytes each).\n */\nexport const MAX_TITLE_BYTES = 80\n\n/**\n * UTF-8 byte length of a string (the host caps titles by encoded size, not\n * character count).\n * @param input - arbitrary text.\n * @returns the number of UTF-8 bytes `input` encodes to.\n */\nexport function utf8ByteLength(input: string): number {\n return new TextEncoder().encode(input).length\n}\n\n/**\n * Byte length of a draft after the same cleaning the host applies before\n * capping: escape/control/directional sequences stripped, whitespace\n * collapsed to single spaces, trimmed. Gating on this value means the editor\n * fires exactly when the host would truncate — whitespace-heavy or control-\n * laced drafts are not falsely flagged, and drafts at the boundary pass\n * through unchanged.\n * @param input - raw editor draft.\n * @returns UTF-8 bytes of the cleaned text (0 when nothing visible remains).\n */\nexport function normalizedTitleByteLength(input: string): number {\n const cleaned = input\n .replace(OSC_SEQUENCE, '')\n .replace(CSI_SEQUENCE, '')\n .replace(ESC_SEQUENCE, '')\n .replace(CONTROL_CHARACTER, '')\n .replace(DIRECTIONAL_CONTROL, '')\n .replace(/\\s+/gu, ' ')\n .trim()\n return utf8ByteLength(cleaned)\n}\n","/**\n * Session-header title-edit entry (`conversation.session.header.actions`): a\n * pencil control immediately right of the title. Editing is IN PLACE: the\n * official current-title crumb is hidden via a `data-ste-inplace` attribute\n * (DOM layer — the official header renders the title and exposes no title\n * seat) and this entry overlays its own input at the crumb's measured rect,\n * so the title itself reads as the input. When the crumb cannot be located\n * (the official DOM changed), the entry degrades to an inline editor in the\n * actions row. Enter commits, Escape cancels, a blurred in-place input\n * cancels, and a trimmed-empty draft never commits; host rejection keeps the\n * editor open with a localized error.\n *\n * TODO(session-title-edit): deprecate the DOM-layer overlay when the official\n * header opens a title slot (or makes the crumb editable) — this entry then\n * becomes a pure slot consumer and the probe/overlay logic is removed.\n */\nimport { useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react'\nimport { Button, IconEditOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'\nimport { isRenameFailure } from './slots.ts'\nimport type { TitleEditActionProps } from './slots.ts'\nimport { MAX_TITLE_BYTES, normalizedTitleByteLength } from './title-length.ts'\nimport css from './TitleEditAction.module.css'\n\n/** One measured placement of the official title crumb (viewport coordinates). */\ninterface CrumbPlacement {\n readonly left: number\n readonly top: number\n readonly width: number\n readonly height: number\n}\n\n/** Attribute hiding the official current-title crumb while editing in place. */\nconst IN_PLACE_ATTR = 'data-ste-inplace'\n\n/** Horizontal padding of the in-place input (`padding: 4px 8px`). */\nconst INPUT_H_PADDING = 8\n/** Border width of the in-place input (`1px solid transparent`). */\nconst INPUT_BORDER = 1\n/** Extra room so the caret and a trailing glyph are never clipped. */\nconst INPUT_CARET_BUFFER = 4\n/** Outer width cap of the fitted input, matching the official crumb's `max-width: 220px`. */\nconst MAX_INPUT_WIDTH = 220\n\n/**\n * Fit the in-place input to its text between the original crumb width (the\n * floor, so the box never collapses while deleting) and the official 220px\n * crumb cap (the ceiling, so a long draft clips exactly where the crumb's\n * ellipsis would).\n * @param textWidth - measured rendered width of the draft text.\n * @param minWidth - the crumb's measured width at open.\n * @returns the outer input width in px.\n */\nfunction fitInputWidth(textWidth: number, minWidth: number): number {\n const target = textWidth + (INPUT_H_PADDING + INPUT_BORDER) * 2 + INPUT_CARET_BUFFER\n return Math.round(Math.min(Math.max(target, minWidth), MAX_INPUT_WIDTH))\n}\n\n/**\n * Locate the official current-title crumb: the header's only disabled crumb\n * button (the current session's crumb is `disabled`, ancestors navigate).\n * @returns the crumb element, or null when the official DOM is unrecognizable.\n */\nfunction locateCrumb(): HTMLButtonElement | null {\n return document.querySelector('header nav button:disabled') as HTMLButtonElement | null\n}\n\n/**\n * Render the title-edit control.\n * @param props - session kit, rename verb, and locale seat.\n * @returns the pencil button, or the in-place / inline editor while editing.\n */\nexport function TitleEditAction({ sessionId, useSessions, renameSession, t }: TitleEditActionProps) {\n const displayTitle = useSessions(state => state.byId[sessionId]?.displayTitle)\n const [editing, setEditing] = useState(false)\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n /** Placement when editing in place (over the hidden crumb); null = row editor. */\n const [placement, setPlacement] = useState<CrumbPlacement | null>(null)\n /** Outer width the in-place input is fitted to (draft text vs the crumb floor and 220px cap). */\n const [inputWidth, setInputWidth] = useState(0)\n const inputRef = useRef<HTMLInputElement | null>(null)\n const crumbRef = useRef<HTMLButtonElement | null>(null)\n /** Hidden mirror of the draft, measured to fit the input to its text. */\n const measureRef = useRef<HTMLSpanElement | null>(null)\n\n // Select the whole current title on open so typing replaces it outright.\n useEffect(() => {\n if (!editing) return\n /* v8 ignore next -- the input is mounted while editing, so the ref is set */\n inputRef.current?.select()\n }, [editing])\n\n // Re-measure while editing in place so a window resize keeps the overlay on\n // the (hidden) crumb's position.\n const inPlaceActive = placement !== null\n useEffect(() => {\n if (!inPlaceActive) return\n const remeasure = (): void => {\n const crumb = crumbRef.current\n /* v8 ignore next -- closeEditor nulls the ref and the placement in the same batch, so a resize cannot fire between them */\n if (crumb === null) return\n const rect = crumb.getBoundingClientRect()\n setPlacement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height })\n }\n window.addEventListener('resize', remeasure)\n return () => { window.removeEventListener('resize', remeasure) }\n }, [inPlaceActive])\n\n // Never leave the official crumb hidden: restore it if this entry unmounts\n // mid-edit (programmatic session switch, HMR reload, plugin disposal).\n useEffect(() => () => {\n if (crumbRef.current !== null) crumbRef.current.removeAttribute(IN_PLACE_ATTR)\n }, [])\n\n // Fit the in-place input to its text: measure the mirror span (which carries\n // the exact draft at the input's font) after every draft or placement\n // change, then clamp between the crumb's original width and the 220px cap.\n useLayoutEffect(() => {\n if (!inPlaceActive) return\n const textWidth = measureRef.current?.offsetWidth ?? 0\n setInputWidth(fitInputWidth(textWidth, placement.width))\n }, [draft, placement, inPlaceActive])\n\n const closeEditor = (): void => {\n setEditing(false)\n setError(null)\n if (crumbRef.current !== null) {\n crumbRef.current.removeAttribute(IN_PLACE_ATTR)\n crumbRef.current = null\n }\n setPlacement(null)\n }\n\n const begin = (): void => {\n setDraft(displayTitle ?? '')\n setError(null)\n const crumb = locateCrumb()\n crumbRef.current = crumb\n if (crumb !== null) {\n // Measure FIRST: the crumb is hidden via visibility (layout box kept),\n // so its rect stays measurable both before and after hiding.\n const rect = crumb.getBoundingClientRect()\n crumb.setAttribute(IN_PLACE_ATTR, '')\n setPlacement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height })\n // Floor the fitted width at the crumb's original width so the open box\n // matches today's look; the fit effect widens it as the draft grows.\n setInputWidth(rect.width)\n }\n setEditing(true)\n }\n const cancel = (): void => {\n /* v8 ignore next -- the cancel button and the input are disabled while busy, so this arm is unreachable through the UI */\n if (busy) return\n closeEditor()\n }\n const commit = async (): Promise<void> => {\n /* v8 ignore next -- the save button and the input are disabled while busy, so this arm is unreachable through the UI */\n if (busy) return\n const title = draft.trim()\n // Never hand the host a draft it would silently truncate: block over-limit\n // commits and surface the localized hint instead (Enter path; the row\n // editor additionally disables save).\n if (title === '' || overLimit) return\n setBusy(true)\n setError(null)\n try {\n await renameSession(title)\n closeEditor()\n } catch (cause) {\n setError(isRenameFailure(cause) && cause.code === 'title-invalid'\n ? t('error.invalid')\n : t('error.rename'))\n } finally {\n setBusy(false)\n }\n }\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === 'Enter') void commit()\n else if (event.key === 'Escape') cancel()\n }\n const onBlur = (): void => {\n if (busy) return\n closeEditor()\n }\n const trimmed = draft.trim()\n // Gate on the host-faithful cleaned byte length: the editor fires exactly\n // when the host would truncate (see title-length.ts), never on whitespace.\n const draftBytes = normalizedTitleByteLength(draft)\n const overLimit = draftBytes > MAX_TITLE_BYTES\n const overLimitHint = t('hint.tooLong', {\n max: MAX_TITLE_BYTES,\n chars: Math.floor(MAX_TITLE_BYTES / 3),\n bytes: draftBytes,\n })\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n className={css.renameButton}\n aria-label={t('action.rename')}\n title={t('action.rename')}\n onClick={begin}\n >\n <IconEditOutline16 size={14} />\n </button>\n )\n }\n if (placement !== null) {\n return (\n <>\n {/* Width-measuring mirror of the draft: hidden, same font as the input. */}\n <span ref={measureRef} className={css.inPlaceMirror} aria-hidden=\"true\">{draft === '' ? ' ' : draft}</span>\n <input\n ref={inputRef}\n className={css.inPlaceInput}\n type=\"text\"\n value={draft}\n aria-label={t('editor.aria')}\n placeholder={t('editor.placeholder')}\n disabled={busy}\n autoFocus\n style={{ left: placement.left, top: placement.top, width: inputWidth, height: placement.height }}\n onChange={(event) => { setDraft(event.currentTarget.value) }}\n onKeyDown={onKeyDown}\n onBlur={onBlur}\n />\n {error !== null\n ? <span className={css.inPlaceError} role=\"alert\">{error}</span>\n : overLimit && <span className={css.inPlaceHint} role=\"alert\">{overLimitHint}</span>}\n </>\n )\n }\n return (\n <span className={css.editor} role=\"group\" aria-label={t('editor.aria')}>\n <input\n ref={inputRef}\n className={css.editorInput}\n type=\"text\"\n value={draft}\n aria-label={t('editor.aria')}\n placeholder={t('editor.placeholder')}\n disabled={busy}\n autoFocus\n onChange={(event) => { setDraft(event.currentTarget.value) }}\n onKeyDown={onKeyDown}\n />\n {error !== null\n ? <span className={css.editorError} role=\"alert\">{error}</span>\n : overLimit && <span className={css.editorHint} role=\"alert\">{overLimitHint}</span>}\n <Button variant=\"outline\" disabled={busy} onClick={cancel}>{t('cancel')}</Button>\n <Button variant=\"primary\" disabled={busy || trimmed === '' || overLimit} onClick={() => { void commit() }}>\n {t('editor.save')}\n </Button>\n </span>\n )\n}\n","/**\n * Session-title-edit plugin, browser half: contributes one\n * `conversation.session.header.actions` entry — a pencil control beside the\n * session title that swaps in an inline editor. The rename verb rides the\n * official session.rename RPC (`session.rename` → `sessions.rename` →\n * `ctx.sessionTitle.rename`), so the plugin needs no host half, no new RPC,\n * and no edits to core packages; the accepted user-sourced `session/title`\n * event pins the title against automatic regeneration. Composing this plugin\n * out of cordis.yml removes every surface it adds.\n * @module @khorsheed/dsh-client-session-title-edit/client\n */\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the ctx.locale service merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls ui-conversation's SlotMap merge\n// ('conversation.session.header.actions').\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { en, NS, zh } from './locales.ts'\nimport type { RenameFailure, SessionTitleEditInjected } from './slots.ts'\nimport { TitleEditAction } from './TitleEditAction.tsx'\n\nexport type { TitleEditActionProps } from './slots.ts'\n\n/** Required services: slot contribution, session binding, and the copy. */\nexport const inject = ['slots', 'sessions', 'locale']\n\n/**\n * Client plugin body: register the dictionaries and the header action.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'session-title-edit: dictionaries')\n ctx.slots.inject(\n 'conversation.session.header.actions',\n () => ctx.slots.register({\n name: 'conversation.session.header.actions',\n id: 'session-title-edit',\n // Lead the actions row: this control is the title's own affordance, so\n // it sits immediately right of the title text, ahead of the static\n // agent-preset context (order -10). The slot convention reserves the\n // negative band for static context, but the user-visible requirement is\n // \"edit beside the title, not after the preset label\".\n order: -20,\n locale: NS,\n inject: (sessionId: SessionId): SessionTitleEditInjected => ({\n renameSession: async (title) => {\n const session = ctx.sessions.binding(sessionId)?.session\n if (session === undefined) {\n throw new Error(`session-title-edit: unknown session \"${sessionId}\"`)\n }\n const result = await session.rename(title)\n if (!result.ok) {\n const failure = new Error(result.error.message) as RenameFailure\n failure.code = result.error.code\n throw failure\n }\n },\n }),\n }, TitleEditAction),\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","useRef","css","IconEditOutline16","Button"],"sources":["../src/client/locales.ts","../src/client/slots.ts","../src/client/title-length.ts","../src/client/TitleEditAction.tsx","../src/client/index.ts"],"sourcesContent":["/** `session-title-edit` namespace dictionaries. */\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'action.rename': '重命名会话',\n 'editor.aria': '会话标题',\n 'editor.placeholder': '输入新标题…',\n 'editor.save': '保存',\n 'cancel': '取消',\n 'error.invalid': '标题不能为空',\n 'error.rename': '重命名失败,请重试',\n 'hint.tooLong': '标题过长:最多 {max} 字节(约 {chars} 个汉字),当前 {bytes} 字节',\n} satisfies Record<string, string>\n\n/** The session-title-edit namespace key union. */\nexport type SessionTitleEditKey = keyof typeof zh\n\n/** The dictionary namespace owned by this plugin. */\nexport const NS = 'session-title-edit'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The session-title editing control's copy. */\n 'session-title-edit': SessionTitleEditKey\n }\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'action.rename': 'Rename session',\n 'editor.aria': 'Session title',\n 'editor.placeholder': 'Enter a new title…',\n 'editor.save': 'Save',\n 'cancel': 'Cancel',\n 'error.invalid': 'The title cannot be empty',\n 'error.rename': 'Rename failed, please try again',\n 'hint.tooLong': 'Title too long: max {max} bytes (about {chars} CJK characters), currently {bytes}',\n} satisfies Record<string, string>\n","/**\n * Slot-facing types of the session-title-edit client half: the injected\n * rename action face and the composed props of its one\n * `conversation.session.header.actions` entry.\n */\nimport type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: pulls ui-conversation's SlotMap merge\n// ('conversation.session.header.actions').\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: pulls this plugin's LocaleNamespaceMap merge.\nimport type {} from './locales.ts'\n\n/** Rename failure carrying the wire error code for localized surfacing. */\nexport interface RenameFailure extends Error {\n /** Wire error code (`session/title-invalid` on the 0.1.2 line, `title-invalid` before); transport failures keep the code the runtime reported. */\n code: string\n}\n\n/**\n * Narrow an unknown thrown value to a {@link RenameFailure}.\n * @param error - the value caught from a rename attempt.\n * @returns true when the value is an Error carrying a string `code`.\n */\nexport function isRenameFailure(error: unknown): error is RenameFailure {\n return error instanceof Error && typeof (error as RenameFailure).code === 'string'\n}\n\n/** Injected action face of the header entry. */\nexport interface SessionTitleEditInjected {\n /**\n * Rename the current session through the official session.rename RPC. The\n * accepted user title pins the session against automatic regeneration.\n * @param title - raw user input; the host normalizes and caps it.\n * @returns when the host accepted the title.\n * @throws {RenameFailure} carrying the wire error code on host rejection.\n */\n renameSession: (title: string) => Promise<void>\n}\n\n/** Full props of the session-header title-edit entry. */\nexport type TitleEditActionProps =\n PropsRuntime<'conversation.session.header.actions'>\n & InjectFace<SessionTitleEditInjected>\n & PropsLocale<'session-title-edit'>\n","/**\n * Client-side mirror of the host session-title byte budget and the\n * normalization the host applies before capping, so the editor can gate\n * against the exact title the host would accept instead of discovering a\n * silent truncation after commit.\n *\n * The host contract: `@deepseek-ai/dsh-session-title`'s\n * `normalizeSessionTitle(input, maxTitleBytes)` cleans the text (ESC/CSI/\n * control/directional sequences stripped, whitespace collapsed to one space,\n * trimmed) and truncates to a UTF-8 byte budget. The production default is\n * `maxTitleBytes: 80` (deepseek-harness `packages/bundle/base/cordis.patch.yml`).\n * The clean step and the constant below mirror that behavior; update them\n * together if the host changes either.\n */\n\n/** Strip operating-system-command escape sequences, including unterminated tails. */\nconst OSC_SEQUENCE = /(?:\\u001B\\]|\\u009D)(?:(?!\\u0007|\\u001B\\\\)[\\s\\S])*(?:\\u0007|\\u001B\\\\|$)/gu\n/** Strip control-sequence-introducer escapes such as SGR color codes. */\nconst CSI_SEQUENCE = /(?:\\u001B\\[|\\u009B)[0-?]*[ -/]*[@-~]/gu\n/** Strip remaining two-byte ESC control sequences. */\nconst ESC_SEQUENCE = /\\u001B[@-_]/gu\n/** Strip non-whitespace C0/C1 control characters. */\nconst CONTROL_CHARACTER = /[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u009F]/gu\n/** Strip directional and invisible controls that can make a displayed title deceptive. */\nconst DIRECTIONAL_CONTROL = /[\\u200B\\u200E\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF]/gu\n\n/**\n * Host cap on any accepted session title, in UTF-8 bytes. Mirrors the\n * production default `maxTitleBytes: 80`; keep in sync with the host config.\n * 80 bytes ≈ 80 ASCII characters or ≈ 26 CJK characters (3 bytes each).\n */\nexport const MAX_TITLE_BYTES = 80\n\n/**\n * UTF-8 byte length of a string (the host caps titles by encoded size, not\n * character count).\n * @param input - arbitrary text.\n * @returns the number of UTF-8 bytes `input` encodes to.\n */\nexport function utf8ByteLength(input: string): number {\n return new TextEncoder().encode(input).length\n}\n\n/**\n * Byte length of a draft after the same cleaning the host applies before\n * capping: escape/control/directional sequences stripped, whitespace\n * collapsed to single spaces, trimmed. Gating on this value means the editor\n * fires exactly when the host would truncate — whitespace-heavy or control-\n * laced drafts are not falsely flagged, and drafts at the boundary pass\n * through unchanged.\n * @param input - raw editor draft.\n * @returns UTF-8 bytes of the cleaned text (0 when nothing visible remains).\n */\nexport function normalizedTitleByteLength(input: string): number {\n const cleaned = input\n .replace(OSC_SEQUENCE, '')\n .replace(CSI_SEQUENCE, '')\n .replace(ESC_SEQUENCE, '')\n .replace(CONTROL_CHARACTER, '')\n .replace(DIRECTIONAL_CONTROL, '')\n .replace(/\\s+/gu, ' ')\n .trim()\n return utf8ByteLength(cleaned)\n}\n","/**\n * Session-header title-edit entry (`conversation.session.header.actions`): a\n * pencil control immediately right of the title. Editing is IN PLACE: the\n * official current-title crumb is hidden via a `data-ste-inplace` attribute\n * (DOM layer — the official header renders the title and exposes no title\n * seat) and this entry overlays its own input at the crumb's measured rect,\n * so the title itself reads as the input. When the crumb cannot be located\n * (the official DOM changed), the entry degrades to an inline editor in the\n * actions row. Enter commits, Escape cancels, a blurred in-place input\n * cancels, and a trimmed-empty draft never commits; host rejection keeps the\n * editor open with a localized error.\n *\n * TODO(session-title-edit): deprecate the DOM-layer overlay when the official\n * header opens a title slot (or makes the crumb editable) — this entry then\n * becomes a pure slot consumer and the probe/overlay logic is removed.\n */\nimport { useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react'\nimport { Button, IconEditOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'\nimport { isRenameFailure } from './slots.ts'\nimport type { TitleEditActionProps } from './slots.ts'\nimport { MAX_TITLE_BYTES, normalizedTitleByteLength } from './title-length.ts'\nimport css from './TitleEditAction.module.css'\n\n/** One measured placement of the official title crumb (viewport coordinates). */\ninterface CrumbPlacement {\n readonly left: number\n readonly top: number\n readonly width: number\n readonly height: number\n}\n\n/** Attribute hiding the official current-title crumb while editing in place. */\nconst IN_PLACE_ATTR = 'data-ste-inplace'\n\n/** Horizontal padding of the in-place input (`padding: 4px 8px`). */\nconst INPUT_H_PADDING = 8\n/** Border width of the in-place input (`1px solid transparent`). */\nconst INPUT_BORDER = 1\n/** Extra room so the caret and a trailing glyph are never clipped. */\nconst INPUT_CARET_BUFFER = 4\n/** Outer width cap of the fitted input, matching the official crumb's `max-width: 220px`. */\nconst MAX_INPUT_WIDTH = 220\n\n/**\n * Fit the in-place input to its text between the original crumb width (the\n * floor, so the box never collapses while deleting) and the official 220px\n * crumb cap (the ceiling, so a long draft clips exactly where the crumb's\n * ellipsis would).\n * @param textWidth - measured rendered width of the draft text.\n * @param minWidth - the crumb's measured width at open.\n * @returns the outer input width in px.\n */\nfunction fitInputWidth(textWidth: number, minWidth: number): number {\n const target = textWidth + (INPUT_H_PADDING + INPUT_BORDER) * 2 + INPUT_CARET_BUFFER\n return Math.round(Math.min(Math.max(target, minWidth), MAX_INPUT_WIDTH))\n}\n\n/**\n * Locate the official current-title crumb: the header's only disabled crumb\n * button (the current session's crumb is `disabled`, ancestors navigate).\n * @returns the crumb element, or null when the official DOM is unrecognizable.\n */\nfunction locateCrumb(): HTMLButtonElement | null {\n return document.querySelector('header nav button:disabled') as HTMLButtonElement | null\n}\n\n/**\n * Render the title-edit control.\n * @param props - session kit, rename verb, and locale seat.\n * @returns the pencil button, or the in-place / inline editor while editing.\n */\nexport function TitleEditAction({ sessionId, useSessions, renameSession, t }: TitleEditActionProps) {\n const displayTitle = useSessions(state => state.byId[sessionId]?.displayTitle)\n const [editing, setEditing] = useState(false)\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n /** Placement when editing in place (over the hidden crumb); null = row editor. */\n const [placement, setPlacement] = useState<CrumbPlacement | null>(null)\n /** Outer width the in-place input is fitted to (draft text vs the crumb floor and 220px cap). */\n const [inputWidth, setInputWidth] = useState(0)\n const inputRef = useRef<HTMLInputElement | null>(null)\n const crumbRef = useRef<HTMLButtonElement | null>(null)\n /** Hidden mirror of the draft, measured to fit the input to its text. */\n const measureRef = useRef<HTMLSpanElement | null>(null)\n\n // Select the whole current title on open so typing replaces it outright.\n useEffect(() => {\n if (!editing) return\n /* v8 ignore next -- the input is mounted while editing, so the ref is set */\n inputRef.current?.select()\n }, [editing])\n\n // Re-measure while editing in place so a window resize keeps the overlay on\n // the (hidden) crumb's position.\n const inPlaceActive = placement !== null\n useEffect(() => {\n if (!inPlaceActive) return\n const remeasure = (): void => {\n const crumb = crumbRef.current\n /* v8 ignore next -- closeEditor nulls the ref and the placement in the same batch, so a resize cannot fire between them */\n if (crumb === null) return\n const rect = crumb.getBoundingClientRect()\n setPlacement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height })\n }\n window.addEventListener('resize', remeasure)\n return () => { window.removeEventListener('resize', remeasure) }\n }, [inPlaceActive])\n\n // Never leave the official crumb hidden: restore it if this entry unmounts\n // mid-edit (programmatic session switch, HMR reload, plugin disposal).\n useEffect(() => () => {\n if (crumbRef.current !== null) crumbRef.current.removeAttribute(IN_PLACE_ATTR)\n }, [])\n\n // Fit the in-place input to its text: measure the mirror span (which carries\n // the exact draft at the input's font) after every draft or placement\n // change, then clamp between the crumb's original width and the 220px cap.\n useLayoutEffect(() => {\n if (!inPlaceActive) return\n const textWidth = measureRef.current?.offsetWidth ?? 0\n setInputWidth(fitInputWidth(textWidth, placement.width))\n }, [draft, placement, inPlaceActive])\n\n const closeEditor = (): void => {\n setEditing(false)\n setError(null)\n if (crumbRef.current !== null) {\n crumbRef.current.removeAttribute(IN_PLACE_ATTR)\n crumbRef.current = null\n }\n setPlacement(null)\n }\n\n const begin = (): void => {\n setDraft(displayTitle ?? '')\n setError(null)\n const crumb = locateCrumb()\n crumbRef.current = crumb\n if (crumb !== null) {\n // Measure FIRST: the crumb is hidden via visibility (layout box kept),\n // so its rect stays measurable both before and after hiding.\n const rect = crumb.getBoundingClientRect()\n crumb.setAttribute(IN_PLACE_ATTR, '')\n setPlacement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height })\n // Floor the fitted width at the crumb's original width so the open box\n // matches today's look; the fit effect widens it as the draft grows.\n setInputWidth(rect.width)\n }\n setEditing(true)\n }\n const cancel = (): void => {\n /* v8 ignore next -- the cancel button and the input are disabled while busy, so this arm is unreachable through the UI */\n if (busy) return\n closeEditor()\n }\n const commit = async (): Promise<void> => {\n /* v8 ignore next -- the save button and the input are disabled while busy, so this arm is unreachable through the UI */\n if (busy) return\n const title = draft.trim()\n // Never hand the host a draft it would silently truncate: block over-limit\n // commits and surface the localized hint instead (Enter path; the row\n // editor additionally disables save).\n if (title === '' || overLimit) return\n setBusy(true)\n setError(null)\n try {\n await renameSession(title)\n closeEditor()\n } catch (cause) {\n setError(isRenameFailure(cause) && (cause.code === 'session/title-invalid' || cause.code === 'title-invalid')\n ? t('error.invalid')\n : t('error.rename'))\n } finally {\n setBusy(false)\n }\n }\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === 'Enter') void commit()\n else if (event.key === 'Escape') cancel()\n }\n const onBlur = (): void => {\n if (busy) return\n closeEditor()\n }\n const trimmed = draft.trim()\n // Gate on the host-faithful cleaned byte length: the editor fires exactly\n // when the host would truncate (see title-length.ts), never on whitespace.\n const draftBytes = normalizedTitleByteLength(draft)\n const overLimit = draftBytes > MAX_TITLE_BYTES\n const overLimitHint = t('hint.tooLong', {\n max: MAX_TITLE_BYTES,\n chars: Math.floor(MAX_TITLE_BYTES / 3),\n bytes: draftBytes,\n })\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n className={css.renameButton}\n aria-label={t('action.rename')}\n title={t('action.rename')}\n onClick={begin}\n >\n <IconEditOutline16 size={14} />\n </button>\n )\n }\n if (placement !== null) {\n return (\n <>\n {/* Width-measuring mirror of the draft: hidden, same font as the input. */}\n <span ref={measureRef} className={css.inPlaceMirror} aria-hidden=\"true\">{draft === '' ? ' ' : draft}</span>\n <input\n ref={inputRef}\n className={css.inPlaceInput}\n type=\"text\"\n value={draft}\n aria-label={t('editor.aria')}\n placeholder={t('editor.placeholder')}\n disabled={busy}\n autoFocus\n style={{ left: placement.left, top: placement.top, width: inputWidth, height: placement.height }}\n onChange={(event) => { setDraft(event.currentTarget.value) }}\n onKeyDown={onKeyDown}\n onBlur={onBlur}\n />\n {error !== null\n ? <span className={css.inPlaceError} role=\"alert\">{error}</span>\n : overLimit && <span className={css.inPlaceHint} role=\"alert\">{overLimitHint}</span>}\n </>\n )\n }\n return (\n <span className={css.editor} role=\"group\" aria-label={t('editor.aria')}>\n <input\n ref={inputRef}\n className={css.editorInput}\n type=\"text\"\n value={draft}\n aria-label={t('editor.aria')}\n placeholder={t('editor.placeholder')}\n disabled={busy}\n autoFocus\n onChange={(event) => { setDraft(event.currentTarget.value) }}\n onKeyDown={onKeyDown}\n />\n {error !== null\n ? <span className={css.editorError} role=\"alert\">{error}</span>\n : overLimit && <span className={css.editorHint} role=\"alert\">{overLimitHint}</span>}\n <Button variant=\"outline\" disabled={busy} onClick={cancel}>{t('cancel')}</Button>\n <Button variant=\"primary\" disabled={busy || trimmed === '' || overLimit} onClick={() => { void commit() }}>\n {t('editor.save')}\n </Button>\n </span>\n )\n}\n","/**\n * Session-title-edit plugin, browser half: contributes one\n * `conversation.session.header.actions` entry — a pencil control beside the\n * session title that swaps in an inline editor. The rename verb rides the\n * official session.rename RPC (`session.rename` → `sessions.rename` →\n * `ctx.sessionTitle.rename`), so the plugin needs no host half, no new RPC,\n * and no edits to core packages; the accepted user-sourced `session/title`\n * event pins the title against automatic regeneration. Composing this plugin\n * out of cordis.yml removes every surface it adds.\n * @module @khorsheed/dsh-client-session-title-edit/client\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: pulls the Controller service merge (ctx.sessions).\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\n// Type-only: pulls the ctx.locale service merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ctx.slots service merge.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: pulls ui-conversation's SlotMap merge\n// ('conversation.session.header.actions').\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { en, NS, zh } from './locales.ts'\nimport type { RenameFailure, SessionTitleEditInjected } from './slots.ts'\nimport { TitleEditAction } from './TitleEditAction.tsx'\n\nexport type { TitleEditActionProps } from './slots.ts'\n\n/** Required services: slot contribution, session binding, and the copy. */\nexport const inject = ['slots', 'sessions', 'locale']\n\n/**\n * Client plugin body: register the dictionaries and the header action.\n * @param ctx - client root context.\n */\nexport function apply(ctx: Context): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'session-title-edit: dictionaries')\n ctx.slots.inject(\n 'conversation.session.header.actions',\n () => ctx.slots.register({\n name: 'conversation.session.header.actions',\n id: 'session-title-edit',\n // Lead the actions row: this control is the title's own affordance, so\n // it sits immediately right of the title text, ahead of the static\n // agent-preset context (order -10). The slot convention reserves the\n // negative band for static context, but the user-visible requirement is\n // \"edit beside the title, not after the preset label\".\n order: -20,\n locale: NS,\n inject: (sessionId: SessionId): SessionTitleEditInjected => ({\n renameSession: async (title) => {\n const session = ctx.sessions.binding(sessionId)?.session\n if (session === undefined) {\n throw new Error(`session-title-edit: unknown session \"${sessionId}\"`)\n }\n const result = await session.rename(title)\n if (!result.ok) {\n const failure = new Error(result.error.message) as RenameFailure\n failure.code = result.error.code\n throw failure\n }\n },\n }),\n }, TitleEditAction),\n )\n}\n"],"mappings":"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"}