@khorsheed/dsh-client-session-title-edit 0.1.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 +11 -0
- package/LICENSE +21 -0
- package/README.en.md +70 -0
- package/README.i18n.yaml +6 -0
- package/README.md +70 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +413 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +11 -0
- package/lib/invariant.js +26 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/client/TitleEditAction.d.ts +8 -0
- package/lib/types/client/TitleEditAction.d.ts.map +1 -0
- package/lib/types/client/TitleEditAction.js +201 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +36 -0
- package/lib/types/client/locales.d.ts +34 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +25 -0
- package/lib/types/client/slots.d.ts +31 -0
- package/lib/types/client/slots.d.ts.map +1 -0
- package/lib/types/client/slots.js +8 -0
- package/lib/types/client/title-length.d.ts +39 -0
- package/lib/types/client/title-length.d.ts.map +1 -0
- package/lib/types/client/title-length.js +60 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +8 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +24 -0
- package/package.json +79 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Session-header title-edit entry (`conversation.session.header.actions`): a
|
|
4
|
+
* pencil control immediately right of the title. Editing is IN PLACE: the
|
|
5
|
+
* official current-title crumb is hidden via a `data-ste-inplace` attribute
|
|
6
|
+
* (DOM layer — the official header renders the title and exposes no title
|
|
7
|
+
* seat) and this entry overlays its own input at the crumb's measured rect,
|
|
8
|
+
* so the title itself reads as the input. When the crumb cannot be located
|
|
9
|
+
* (the official DOM changed), the entry degrades to an inline editor in the
|
|
10
|
+
* actions row. Enter commits, Escape cancels, a blurred in-place input
|
|
11
|
+
* cancels, and a trimmed-empty draft never commits; host rejection keeps the
|
|
12
|
+
* editor open with a localized error.
|
|
13
|
+
*
|
|
14
|
+
* TODO(session-title-edit): deprecate the DOM-layer overlay when the official
|
|
15
|
+
* header opens a title slot (or makes the crumb editable) — this entry then
|
|
16
|
+
* becomes a pure slot consumer and the probe/overlay logic is removed.
|
|
17
|
+
*/
|
|
18
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
19
|
+
import { Button, IconEditOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
20
|
+
import { isRenameFailure } from './slots.js';
|
|
21
|
+
import { MAX_TITLE_BYTES, normalizedTitleByteLength } from './title-length.js';
|
|
22
|
+
import css from './TitleEditAction.module.css';
|
|
23
|
+
/** Attribute hiding the official current-title crumb while editing in place. */
|
|
24
|
+
const IN_PLACE_ATTR = 'data-ste-inplace';
|
|
25
|
+
/** Horizontal padding of the in-place input (`padding: 4px 8px`). */
|
|
26
|
+
const INPUT_H_PADDING = 8;
|
|
27
|
+
/** Border width of the in-place input (`1px solid transparent`). */
|
|
28
|
+
const INPUT_BORDER = 1;
|
|
29
|
+
/** Extra room so the caret and a trailing glyph are never clipped. */
|
|
30
|
+
const INPUT_CARET_BUFFER = 4;
|
|
31
|
+
/** Outer width cap of the fitted input, matching the official crumb's `max-width: 220px`. */
|
|
32
|
+
const MAX_INPUT_WIDTH = 220;
|
|
33
|
+
/**
|
|
34
|
+
* Fit the in-place input to its text between the original crumb width (the
|
|
35
|
+
* floor, so the box never collapses while deleting) and the official 220px
|
|
36
|
+
* crumb cap (the ceiling, so a long draft clips exactly where the crumb's
|
|
37
|
+
* ellipsis would).
|
|
38
|
+
* @param textWidth - measured rendered width of the draft text.
|
|
39
|
+
* @param minWidth - the crumb's measured width at open.
|
|
40
|
+
* @returns the outer input width in px.
|
|
41
|
+
*/
|
|
42
|
+
function fitInputWidth(textWidth, minWidth) {
|
|
43
|
+
const target = textWidth + (INPUT_H_PADDING + INPUT_BORDER) * 2 + INPUT_CARET_BUFFER;
|
|
44
|
+
return Math.round(Math.min(Math.max(target, minWidth), MAX_INPUT_WIDTH));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Locate the official current-title crumb: the header's only disabled crumb
|
|
48
|
+
* button (the current session's crumb is `disabled`, ancestors navigate).
|
|
49
|
+
* @returns the crumb element, or null when the official DOM is unrecognizable.
|
|
50
|
+
*/
|
|
51
|
+
function locateCrumb() {
|
|
52
|
+
return document.querySelector('header nav button:disabled');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render the title-edit control.
|
|
56
|
+
* @param props - session kit, rename verb, and locale seat.
|
|
57
|
+
* @returns the pencil button, or the in-place / inline editor while editing.
|
|
58
|
+
*/
|
|
59
|
+
export function TitleEditAction({ sessionId, useSessions, renameSession, t }) {
|
|
60
|
+
const displayTitle = useSessions(state => state.byId[sessionId]?.displayTitle);
|
|
61
|
+
const [editing, setEditing] = useState(false);
|
|
62
|
+
const [draft, setDraft] = useState('');
|
|
63
|
+
const [busy, setBusy] = useState(false);
|
|
64
|
+
const [error, setError] = useState(null);
|
|
65
|
+
/** Placement when editing in place (over the hidden crumb); null = row editor. */
|
|
66
|
+
const [placement, setPlacement] = useState(null);
|
|
67
|
+
/** Outer width the in-place input is fitted to (draft text vs the crumb floor and 220px cap). */
|
|
68
|
+
const [inputWidth, setInputWidth] = useState(0);
|
|
69
|
+
const inputRef = useRef(null);
|
|
70
|
+
const crumbRef = useRef(null);
|
|
71
|
+
/** Hidden mirror of the draft, measured to fit the input to its text. */
|
|
72
|
+
const measureRef = useRef(null);
|
|
73
|
+
// Select the whole current title on open so typing replaces it outright.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!editing)
|
|
76
|
+
return;
|
|
77
|
+
/* v8 ignore next -- the input is mounted while editing, so the ref is set */
|
|
78
|
+
inputRef.current?.select();
|
|
79
|
+
}, [editing]);
|
|
80
|
+
// Re-measure while editing in place so a window resize keeps the overlay on
|
|
81
|
+
// the (hidden) crumb's position.
|
|
82
|
+
const inPlaceActive = placement !== null;
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!inPlaceActive)
|
|
85
|
+
return;
|
|
86
|
+
const remeasure = () => {
|
|
87
|
+
const crumb = crumbRef.current;
|
|
88
|
+
/* v8 ignore next -- closeEditor nulls the ref and the placement in the same batch, so a resize cannot fire between them */
|
|
89
|
+
if (crumb === null)
|
|
90
|
+
return;
|
|
91
|
+
const rect = crumb.getBoundingClientRect();
|
|
92
|
+
setPlacement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height });
|
|
93
|
+
};
|
|
94
|
+
window.addEventListener('resize', remeasure);
|
|
95
|
+
return () => { window.removeEventListener('resize', remeasure); };
|
|
96
|
+
}, [inPlaceActive]);
|
|
97
|
+
// Never leave the official crumb hidden: restore it if this entry unmounts
|
|
98
|
+
// mid-edit (programmatic session switch, HMR reload, plugin disposal).
|
|
99
|
+
useEffect(() => () => {
|
|
100
|
+
if (crumbRef.current !== null)
|
|
101
|
+
crumbRef.current.removeAttribute(IN_PLACE_ATTR);
|
|
102
|
+
}, []);
|
|
103
|
+
// Fit the in-place input to its text: measure the mirror span (which carries
|
|
104
|
+
// the exact draft at the input's font) after every draft or placement
|
|
105
|
+
// change, then clamp between the crumb's original width and the 220px cap.
|
|
106
|
+
useLayoutEffect(() => {
|
|
107
|
+
if (!inPlaceActive)
|
|
108
|
+
return;
|
|
109
|
+
const textWidth = measureRef.current?.offsetWidth ?? 0;
|
|
110
|
+
setInputWidth(fitInputWidth(textWidth, placement.width));
|
|
111
|
+
}, [draft, placement, inPlaceActive]);
|
|
112
|
+
const closeEditor = () => {
|
|
113
|
+
setEditing(false);
|
|
114
|
+
setError(null);
|
|
115
|
+
if (crumbRef.current !== null) {
|
|
116
|
+
crumbRef.current.removeAttribute(IN_PLACE_ATTR);
|
|
117
|
+
crumbRef.current = null;
|
|
118
|
+
}
|
|
119
|
+
setPlacement(null);
|
|
120
|
+
};
|
|
121
|
+
const begin = () => {
|
|
122
|
+
setDraft(displayTitle ?? '');
|
|
123
|
+
setError(null);
|
|
124
|
+
const crumb = locateCrumb();
|
|
125
|
+
crumbRef.current = crumb;
|
|
126
|
+
if (crumb !== null) {
|
|
127
|
+
// Measure FIRST: the crumb is hidden via visibility (layout box kept),
|
|
128
|
+
// so its rect stays measurable both before and after hiding.
|
|
129
|
+
const rect = crumb.getBoundingClientRect();
|
|
130
|
+
crumb.setAttribute(IN_PLACE_ATTR, '');
|
|
131
|
+
setPlacement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height });
|
|
132
|
+
// Floor the fitted width at the crumb's original width so the open box
|
|
133
|
+
// matches today's look; the fit effect widens it as the draft grows.
|
|
134
|
+
setInputWidth(rect.width);
|
|
135
|
+
}
|
|
136
|
+
setEditing(true);
|
|
137
|
+
};
|
|
138
|
+
const cancel = () => {
|
|
139
|
+
/* v8 ignore next -- the cancel button and the input are disabled while busy, so this arm is unreachable through the UI */
|
|
140
|
+
if (busy)
|
|
141
|
+
return;
|
|
142
|
+
closeEditor();
|
|
143
|
+
};
|
|
144
|
+
const commit = async () => {
|
|
145
|
+
/* v8 ignore next -- the save button and the input are disabled while busy, so this arm is unreachable through the UI */
|
|
146
|
+
if (busy)
|
|
147
|
+
return;
|
|
148
|
+
const title = draft.trim();
|
|
149
|
+
// Never hand the host a draft it would silently truncate: block over-limit
|
|
150
|
+
// commits and surface the localized hint instead (Enter path; the row
|
|
151
|
+
// editor additionally disables save).
|
|
152
|
+
if (title === '' || overLimit)
|
|
153
|
+
return;
|
|
154
|
+
setBusy(true);
|
|
155
|
+
setError(null);
|
|
156
|
+
try {
|
|
157
|
+
await renameSession(title);
|
|
158
|
+
closeEditor();
|
|
159
|
+
}
|
|
160
|
+
catch (cause) {
|
|
161
|
+
setError(isRenameFailure(cause) && cause.code === 'title-invalid'
|
|
162
|
+
? t('error.invalid')
|
|
163
|
+
: t('error.rename'));
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
setBusy(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const onKeyDown = (event) => {
|
|
170
|
+
if (event.key === 'Enter')
|
|
171
|
+
void commit();
|
|
172
|
+
else if (event.key === 'Escape')
|
|
173
|
+
cancel();
|
|
174
|
+
};
|
|
175
|
+
const onBlur = () => {
|
|
176
|
+
if (busy)
|
|
177
|
+
return;
|
|
178
|
+
closeEditor();
|
|
179
|
+
};
|
|
180
|
+
const trimmed = draft.trim();
|
|
181
|
+
// Gate on the host-faithful cleaned byte length: the editor fires exactly
|
|
182
|
+
// when the host would truncate (see title-length.ts), never on whitespace.
|
|
183
|
+
const draftBytes = normalizedTitleByteLength(draft);
|
|
184
|
+
const overLimit = draftBytes > MAX_TITLE_BYTES;
|
|
185
|
+
const overLimitHint = t('hint.tooLong', {
|
|
186
|
+
max: MAX_TITLE_BYTES,
|
|
187
|
+
chars: Math.floor(MAX_TITLE_BYTES / 3),
|
|
188
|
+
bytes: draftBytes,
|
|
189
|
+
});
|
|
190
|
+
if (!editing) {
|
|
191
|
+
return (_jsx("button", { type: "button", className: css.renameButton, "aria-label": t('action.rename'), title: t('action.rename'), onClick: begin, children: _jsx(IconEditOutline16, { size: 14 }) }));
|
|
192
|
+
}
|
|
193
|
+
if (placement !== null) {
|
|
194
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { ref: measureRef, className: css.inPlaceMirror, "aria-hidden": "true", children: draft === '' ? ' ' : draft }), _jsx("input", { ref: inputRef, className: css.inPlaceInput, type: "text", value: draft, "aria-label": t('editor.aria'), placeholder: t('editor.placeholder'), disabled: busy, autoFocus: true, style: { left: placement.left, top: placement.top, width: inputWidth, height: placement.height }, onChange: (event) => { setDraft(event.currentTarget.value); }, onKeyDown: onKeyDown, onBlur: onBlur }), error !== null
|
|
195
|
+
? _jsx("span", { className: css.inPlaceError, role: "alert", children: error })
|
|
196
|
+
: overLimit && _jsx("span", { className: css.inPlaceHint, role: "alert", children: overLimitHint })] }));
|
|
197
|
+
}
|
|
198
|
+
return (_jsxs("span", { className: css.editor, role: "group", "aria-label": t('editor.aria'), children: [_jsx("input", { ref: inputRef, className: css.editorInput, type: "text", value: draft, "aria-label": t('editor.aria'), placeholder: t('editor.placeholder'), disabled: busy, autoFocus: true, onChange: (event) => { setDraft(event.currentTarget.value); }, onKeyDown: onKeyDown }), error !== null
|
|
199
|
+
? _jsx("span", { className: css.editorError, role: "alert", children: error })
|
|
200
|
+
: overLimit && _jsx("span", { className: css.editorHint, role: "alert", children: overLimitHint }), _jsx(Button, { variant: "outline", disabled: busy, onClick: cancel, children: t('cancel') }), _jsx(Button, { variant: "primary", disabled: busy || trimmed === '' || overLimit, onClick: () => { void commit(); }, children: t('editor.save') })] }));
|
|
201
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-title-edit plugin, browser half: contributes one
|
|
3
|
+
* `conversation.session.header.actions` entry — a pencil control beside the
|
|
4
|
+
* session title that swaps in an inline editor. The rename verb rides the
|
|
5
|
+
* official session.rename RPC (`session.rename` → `sessions.rename` →
|
|
6
|
+
* `ctx.sessionTitle.rename`), so the plugin needs no host half, no new RPC,
|
|
7
|
+
* and no edits to core packages; the accepted user-sourced `session/title`
|
|
8
|
+
* event pins the title against automatic regeneration. Composing this plugin
|
|
9
|
+
* out of cordis.yml removes every surface it adds.
|
|
10
|
+
* @module @khorsheed/dsh-client-session-title-edit/client
|
|
11
|
+
*/
|
|
12
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
13
|
+
export type { TitleEditActionProps } from './slots.ts';
|
|
14
|
+
/** Required services: slot contribution, session binding, and the copy. */
|
|
15
|
+
export declare const inject: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Client plugin body: register the dictionaries and the header action.
|
|
18
|
+
* @param ctx - client root context.
|
|
19
|
+
*/
|
|
20
|
+
export declare function apply(ctx: ClientContext): void;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,wCAAwC,CAAA;AAUtF,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,MAAM,UAAkC,CAAA;AAErD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA8B9C"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { en, NS, zh } from './locales.js';
|
|
2
|
+
import { TitleEditAction } from './TitleEditAction.js';
|
|
3
|
+
/** Required services: slot contribution, session binding, and the copy. */
|
|
4
|
+
export const inject = ['slots', 'sessions', 'locale'];
|
|
5
|
+
/**
|
|
6
|
+
* Client plugin body: register the dictionaries and the header action.
|
|
7
|
+
* @param ctx - client root context.
|
|
8
|
+
*/
|
|
9
|
+
export function apply(ctx) {
|
|
10
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'session-title-edit: dictionaries');
|
|
11
|
+
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
|
|
12
|
+
name: 'conversation.session.header.actions',
|
|
13
|
+
id: 'session-title-edit',
|
|
14
|
+
// Lead the actions row: this control is the title's own affordance, so
|
|
15
|
+
// it sits immediately right of the title text, ahead of the static
|
|
16
|
+
// agent-preset context (order -10). The slot convention reserves the
|
|
17
|
+
// negative band for static context, but the user-visible requirement is
|
|
18
|
+
// "edit beside the title, not after the preset label".
|
|
19
|
+
order: -20,
|
|
20
|
+
locale: NS,
|
|
21
|
+
inject: (sessionId) => ({
|
|
22
|
+
renameSession: async (title) => {
|
|
23
|
+
const session = ctx.sessions.binding(sessionId)?.session;
|
|
24
|
+
if (session === undefined) {
|
|
25
|
+
throw new Error(`session-title-edit: unknown session "${sessionId}"`);
|
|
26
|
+
}
|
|
27
|
+
const result = await session.rename(title);
|
|
28
|
+
if (!result.ok) {
|
|
29
|
+
const failure = new Error(result.error.message);
|
|
30
|
+
failure.code = result.error.code;
|
|
31
|
+
throw failure;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
}, TitleEditAction));
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** `session-title-edit` namespace dictionaries. */
|
|
2
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
3
|
+
export declare const zh: {
|
|
4
|
+
'action.rename': string;
|
|
5
|
+
'editor.aria': string;
|
|
6
|
+
'editor.placeholder': string;
|
|
7
|
+
'editor.save': string;
|
|
8
|
+
cancel: string;
|
|
9
|
+
'error.invalid': string;
|
|
10
|
+
'error.rename': string;
|
|
11
|
+
'hint.tooLong': string;
|
|
12
|
+
};
|
|
13
|
+
/** The session-title-edit namespace key union. */
|
|
14
|
+
export type SessionTitleEditKey = keyof typeof zh;
|
|
15
|
+
/** The dictionary namespace owned by this plugin. */
|
|
16
|
+
export declare const NS = "session-title-edit";
|
|
17
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
18
|
+
interface LocaleNamespaceMap {
|
|
19
|
+
/** The session-title editing control's copy. */
|
|
20
|
+
'session-title-edit': SessionTitleEditKey;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
24
|
+
export declare const en: {
|
|
25
|
+
'action.rename': string;
|
|
26
|
+
'editor.aria': string;
|
|
27
|
+
'editor.placeholder': string;
|
|
28
|
+
'editor.save': string;
|
|
29
|
+
cancel: string;
|
|
30
|
+
'error.invalid': string;
|
|
31
|
+
'error.rename': string;
|
|
32
|
+
'hint.tooLong': string;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,mDAAmD;AAEnD,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;CASmB,CAAA;AAElC,kDAAkD;AAClD,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,EAAE,CAAA;AAEjD,qDAAqD;AACrD,eAAO,MAAM,EAAE,uBAAuB,CAAA;AAEtC,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,gDAAgD;QAChD,oBAAoB,EAAE,mBAAmB,CAAA;KAC1C;CACF;AAED,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;CASmB,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** `session-title-edit` namespace dictionaries. */
|
|
2
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
3
|
+
export const zh = {
|
|
4
|
+
'action.rename': '重命名会话',
|
|
5
|
+
'editor.aria': '会话标题',
|
|
6
|
+
'editor.placeholder': '输入新标题…',
|
|
7
|
+
'editor.save': '保存',
|
|
8
|
+
'cancel': '取消',
|
|
9
|
+
'error.invalid': '标题不能为空',
|
|
10
|
+
'error.rename': '重命名失败,请重试',
|
|
11
|
+
'hint.tooLong': '标题过长:最多 {max} 字节(约 {chars} 个汉字),当前 {bytes} 字节',
|
|
12
|
+
};
|
|
13
|
+
/** The dictionary namespace owned by this plugin. */
|
|
14
|
+
export const NS = 'session-title-edit';
|
|
15
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
16
|
+
export const en = {
|
|
17
|
+
'action.rename': 'Rename session',
|
|
18
|
+
'editor.aria': 'Session title',
|
|
19
|
+
'editor.placeholder': 'Enter a new title…',
|
|
20
|
+
'editor.save': 'Save',
|
|
21
|
+
'cancel': 'Cancel',
|
|
22
|
+
'error.invalid': 'The title cannot be empty',
|
|
23
|
+
'error.rename': 'Rename failed, please try again',
|
|
24
|
+
'hint.tooLong': 'Title too long: max {max} bytes (about {chars} CJK characters), currently {bytes}',
|
|
25
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot-facing types of the session-title-edit client half: the injected
|
|
3
|
+
* rename action face and the composed props of its one
|
|
4
|
+
* `conversation.session.header.actions` entry.
|
|
5
|
+
*/
|
|
6
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
7
|
+
/** Rename failure carrying the wire error code for localized surfacing. */
|
|
8
|
+
export interface RenameFailure extends Error {
|
|
9
|
+
/** Wire error code (e.g. `title-invalid`); transport failures keep the code the runtime reported. */
|
|
10
|
+
code: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Narrow an unknown thrown value to a {@link RenameFailure}.
|
|
14
|
+
* @param error - the value caught from a rename attempt.
|
|
15
|
+
* @returns true when the value is an Error carrying a string `code`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isRenameFailure(error: unknown): error is RenameFailure;
|
|
18
|
+
/** Injected action face of the header entry. */
|
|
19
|
+
export interface SessionTitleEditInjected {
|
|
20
|
+
/**
|
|
21
|
+
* Rename the current session through the official session.rename RPC. The
|
|
22
|
+
* accepted user title pins the session against automatic regeneration.
|
|
23
|
+
* @param title - raw user input; the host normalizes and caps it.
|
|
24
|
+
* @returns when the host accepted the title.
|
|
25
|
+
* @throws {RenameFailure} carrying the wire error code on host rejection.
|
|
26
|
+
*/
|
|
27
|
+
renameSession: (title: string) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** Full props of the session-header title-edit entry. */
|
|
30
|
+
export type TitleEditActionProps = PropsRuntime<'conversation.session.header.actions'> & InjectFace<SessionTitleEditInjected> & PropsLocale<'session-title-edit'>;
|
|
31
|
+
//# sourceMappingURL=slots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../../src/client/slots.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAO7F,2EAA2E;AAC3E,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,qGAAqG;IACrG,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAED,gDAAgD;AAChD,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD;AAED,yDAAyD;AACzD,MAAM,MAAM,oBAAoB,GAC9B,YAAY,CAAC,qCAAqC,CAAC,GACjD,UAAU,CAAC,wBAAwB,CAAC,GACpC,WAAW,CAAC,oBAAoB,CAAC,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow an unknown thrown value to a {@link RenameFailure}.
|
|
3
|
+
* @param error - the value caught from a rename attempt.
|
|
4
|
+
* @returns true when the value is an Error carrying a string `code`.
|
|
5
|
+
*/
|
|
6
|
+
export function isRenameFailure(error) {
|
|
7
|
+
return error instanceof Error && typeof error.code === 'string';
|
|
8
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side mirror of the host session-title byte budget and the
|
|
3
|
+
* normalization the host applies before capping, so the editor can gate
|
|
4
|
+
* against the exact title the host would accept instead of discovering a
|
|
5
|
+
* silent truncation after commit.
|
|
6
|
+
*
|
|
7
|
+
* The host contract: `@deepseek-ai/dsh-session-title`'s
|
|
8
|
+
* `normalizeSessionTitle(input, maxTitleBytes)` cleans the text (ESC/CSI/
|
|
9
|
+
* control/directional sequences stripped, whitespace collapsed to one space,
|
|
10
|
+
* trimmed) and truncates to a UTF-8 byte budget. The production default is
|
|
11
|
+
* `maxTitleBytes: 80` (deepseek-harness `packages/bundle/base/cordis.patch.yml`).
|
|
12
|
+
* The clean step and the constant below mirror that behavior; update them
|
|
13
|
+
* together if the host changes either.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Host cap on any accepted session title, in UTF-8 bytes. Mirrors the
|
|
17
|
+
* production default `maxTitleBytes: 80`; keep in sync with the host config.
|
|
18
|
+
* 80 bytes ≈ 80 ASCII characters or ≈ 26 CJK characters (3 bytes each).
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_TITLE_BYTES = 80;
|
|
21
|
+
/**
|
|
22
|
+
* UTF-8 byte length of a string (the host caps titles by encoded size, not
|
|
23
|
+
* character count).
|
|
24
|
+
* @param input - arbitrary text.
|
|
25
|
+
* @returns the number of UTF-8 bytes `input` encodes to.
|
|
26
|
+
*/
|
|
27
|
+
export declare function utf8ByteLength(input: string): number;
|
|
28
|
+
/**
|
|
29
|
+
* Byte length of a draft after the same cleaning the host applies before
|
|
30
|
+
* capping: escape/control/directional sequences stripped, whitespace
|
|
31
|
+
* collapsed to single spaces, trimmed. Gating on this value means the editor
|
|
32
|
+
* fires exactly when the host would truncate — whitespace-heavy or control-
|
|
33
|
+
* laced drafts are not falsely flagged, and drafts at the boundary pass
|
|
34
|
+
* through unchanged.
|
|
35
|
+
* @param input - raw editor draft.
|
|
36
|
+
* @returns UTF-8 bytes of the cleaned text (0 when nothing visible remains).
|
|
37
|
+
*/
|
|
38
|
+
export declare function normalizedTitleByteLength(input: string): number;
|
|
39
|
+
//# sourceMappingURL=title-length.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"title-length.d.ts","sourceRoot":"","sources":["../../../src/client/title-length.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH;;;;GAIG;AACH,eAAO,MAAM,eAAe,KAAK,CAAA;AAEjC;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAU/D"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side mirror of the host session-title byte budget and the
|
|
3
|
+
* normalization the host applies before capping, so the editor can gate
|
|
4
|
+
* against the exact title the host would accept instead of discovering a
|
|
5
|
+
* silent truncation after commit.
|
|
6
|
+
*
|
|
7
|
+
* The host contract: `@deepseek-ai/dsh-session-title`'s
|
|
8
|
+
* `normalizeSessionTitle(input, maxTitleBytes)` cleans the text (ESC/CSI/
|
|
9
|
+
* control/directional sequences stripped, whitespace collapsed to one space,
|
|
10
|
+
* trimmed) and truncates to a UTF-8 byte budget. The production default is
|
|
11
|
+
* `maxTitleBytes: 80` (deepseek-harness `packages/bundle/base/cordis.patch.yml`).
|
|
12
|
+
* The clean step and the constant below mirror that behavior; update them
|
|
13
|
+
* together if the host changes either.
|
|
14
|
+
*/
|
|
15
|
+
/** Strip operating-system-command escape sequences, including unterminated tails. */
|
|
16
|
+
const OSC_SEQUENCE = /(?:\u001B\]|\u009D)(?:(?!\u0007|\u001B\\)[\s\S])*(?:\u0007|\u001B\\|$)/gu;
|
|
17
|
+
/** Strip control-sequence-introducer escapes such as SGR color codes. */
|
|
18
|
+
const CSI_SEQUENCE = /(?:\u001B\[|\u009B)[0-?]*[ -/]*[@-~]/gu;
|
|
19
|
+
/** Strip remaining two-byte ESC control sequences. */
|
|
20
|
+
const ESC_SEQUENCE = /\u001B[@-_]/gu;
|
|
21
|
+
/** Strip non-whitespace C0/C1 control characters. */
|
|
22
|
+
const CONTROL_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu;
|
|
23
|
+
/** Strip directional and invisible controls that can make a displayed title deceptive. */
|
|
24
|
+
const DIRECTIONAL_CONTROL = /[\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF]/gu;
|
|
25
|
+
/**
|
|
26
|
+
* Host cap on any accepted session title, in UTF-8 bytes. Mirrors the
|
|
27
|
+
* production default `maxTitleBytes: 80`; keep in sync with the host config.
|
|
28
|
+
* 80 bytes ≈ 80 ASCII characters or ≈ 26 CJK characters (3 bytes each).
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_TITLE_BYTES = 80;
|
|
31
|
+
/**
|
|
32
|
+
* UTF-8 byte length of a string (the host caps titles by encoded size, not
|
|
33
|
+
* character count).
|
|
34
|
+
* @param input - arbitrary text.
|
|
35
|
+
* @returns the number of UTF-8 bytes `input` encodes to.
|
|
36
|
+
*/
|
|
37
|
+
export function utf8ByteLength(input) {
|
|
38
|
+
return new TextEncoder().encode(input).length;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Byte length of a draft after the same cleaning the host applies before
|
|
42
|
+
* capping: escape/control/directional sequences stripped, whitespace
|
|
43
|
+
* collapsed to single spaces, trimmed. Gating on this value means the editor
|
|
44
|
+
* fires exactly when the host would truncate — whitespace-heavy or control-
|
|
45
|
+
* laced drafts are not falsely flagged, and drafts at the boundary pass
|
|
46
|
+
* through unchanged.
|
|
47
|
+
* @param input - raw editor draft.
|
|
48
|
+
* @returns UTF-8 bytes of the cleaned text (0 when nothing visible remains).
|
|
49
|
+
*/
|
|
50
|
+
export function normalizedTitleByteLength(input) {
|
|
51
|
+
const cleaned = input
|
|
52
|
+
.replace(OSC_SEQUENCE, '')
|
|
53
|
+
.replace(CSI_SEQUENCE, '')
|
|
54
|
+
.replace(ESC_SEQUENCE, '')
|
|
55
|
+
.replace(CONTROL_CHARACTER, '')
|
|
56
|
+
.replace(DIRECTIONAL_CONTROL, '')
|
|
57
|
+
.replace(/\s+/gu, ' ')
|
|
58
|
+
.trim();
|
|
59
|
+
return utf8ByteLength(cleaned);
|
|
60
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-title-edit plugin, node half. Pure UI plugin: the empty apply
|
|
3
|
+
* exists so the plugin appears in the host cordis.yml / Loader; the browser
|
|
4
|
+
* half ships via exports["./client"], discovered through the package.json
|
|
5
|
+
* dsh.client declaration.
|
|
6
|
+
*/
|
|
7
|
+
/** Host plugin body — no host-side behavior for this UI plugin. */
|
|
8
|
+
export declare function apply(): void;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mEAAmE;AACnE,wBAAgB,KAAK,IAAI,IAAI,CAAG"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-title-edit plugin, node half. Pure UI plugin: the empty apply
|
|
3
|
+
* exists so the plugin appears in the host cordis.yml / Loader; the browser
|
|
4
|
+
* half ships via exports["./client"], discovered through the package.json
|
|
5
|
+
* dsh.client declaration.
|
|
6
|
+
*/
|
|
7
|
+
/** Host plugin body — no host-side behavior for this UI plugin. */
|
|
8
|
+
export function apply() { }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-client-session-title-edit`.
|
|
3
|
+
* @module @khorsheed/dsh-client-session-title-edit/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "client-session-title-edit-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariant.d.ts","sourceRoot":"","sources":["../../src/invariant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKlD,oCAAoC;AACpC,eAAO,MAAM,IAAI,wCAAwC,CAAA;AACzD,2EAA2E;AAC3E,eAAO,MAAM,MAAM,UAAiB,CAAA;AAWpC;;;;GAIG;AACH,eAAO,MAAM,KAAK,QAAS,OAAO,KAAG,OAAO,CAAC,MAAM,IAAI,CACU,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-client-session-title-edit`.
|
|
3
|
+
* @module @khorsheed/dsh-client-session-title-edit/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@khorsheed/dsh-client-session-title-edit';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'client-session-title-edit-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: every accepted rename is appended through the official
|
|
12
|
+
* session service, whose session-title invariant companion audits the
|
|
13
|
+
* `session/title` source relationship (a user rename must carry no message
|
|
14
|
+
* citations), and the plugin's one header-slot entry proves disposal through
|
|
15
|
+
* the browser-plugin spec. No second authority exists to check at runtime.
|
|
16
|
+
*/
|
|
17
|
+
const install = () => { };
|
|
18
|
+
/**
|
|
19
|
+
* Register this package's invariant companion.
|
|
20
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
21
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
22
|
+
*/
|
|
23
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
24
|
+
/* jscpd:ignore-end */
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khorsheed/dsh-client-session-title-edit",
|
|
3
|
+
"description": "Session title editing in the web GUI chat header: an edit control beside the title that renames the current session through the official session.rename RPC (the user-sourced title pins against automatic regeneration)",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"dsh",
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"cordis",
|
|
8
|
+
"session",
|
|
9
|
+
"title"
|
|
10
|
+
],
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"types": "lib/types/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./lib/types/index.d.ts",
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./invariant": {
|
|
21
|
+
"types": "./lib/types/invariant.d.ts",
|
|
22
|
+
"default": "./lib/invariant.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./lib/types/client/index.d.ts",
|
|
26
|
+
"default": "./lib/client.js"
|
|
27
|
+
},
|
|
28
|
+
"./src/*": "./src/*",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
35
|
+
"client": {
|
|
36
|
+
"inject": [
|
|
37
|
+
"@deepseek-ai/dsh-client-locale",
|
|
38
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
41
|
+
],
|
|
42
|
+
"platform": "web"
|
|
43
|
+
},
|
|
44
|
+
"compat": {
|
|
45
|
+
"minHost": "0.1.0-rc.6",
|
|
46
|
+
"verifiedHost": "0.1.1-rc.2"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
53
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
58
|
+
"react": "^18.2.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.1.1-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
|
|
69
|
+
"@types/react": "~18.3.1",
|
|
70
|
+
"react": "^18.2.0",
|
|
71
|
+
"react-dom": "^18.2.0"
|
|
72
|
+
},
|
|
73
|
+
"files": [
|
|
74
|
+
"lib",
|
|
75
|
+
"cordis.patch.yml",
|
|
76
|
+
"LICENSE",
|
|
77
|
+
"CHANGELOG.md"
|
|
78
|
+
]
|
|
79
|
+
}
|