dsh-prompt-history 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/LICENSE +21 -0
- package/README.md +47 -0
- package/README_EN.md +47 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +180 -0
- package/lib/index.js +11 -0
- package/lib/invariant.js +25 -0
- package/lib/types/client/InputHistory.d.ts +10 -0
- package/lib/types/client/index.d.ts +17 -0
- package/lib/types/client/index.js +13 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/index.js +8 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/package.json +95 -0
- package/src/client/InputHistory.tsx +171 -0
- package/src/client/index.ts +27 -0
- package/src/index.ts +9 -0
- package/src/invariant.ts +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-prompt-history contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# dsh-prompt-history
|
|
2
|
+
|
|
3
|
+
DSH Web 输入框的「类 Linux shell」提示词历史插件:在输入框里按 **↑ / ↓** 上下翻看之前发送过的消息。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
dsh plugin --profile web add dsh-prompt-history
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
然后刷新页面即可使用。
|
|
12
|
+
|
|
13
|
+
## 用法
|
|
14
|
+
|
|
15
|
+
- **↑**:召回上一条发送过的消息(最新在前),替换当前输入;继续按 ↑ 往前翻
|
|
16
|
+
- **↓**:往后翻;翻到底时恢复你翻历史之前正在输入的那一行(readline 的 pending-line 行为)
|
|
17
|
+
- **编辑即退出**:浏览历史时一旦动手编辑,自动回到当前行,不再继续翻
|
|
18
|
+
- **不干扰其它操作**:`/` 与 `@` 建议菜单打开时,方向键仍归菜单导航;中文输入法(IME)组合、`Shift+↑` 选区、`Ctrl+↑` 跳词等修饰键组合均不拦截;发送中/已删除会话不响应
|
|
19
|
+
|
|
20
|
+
## 特性
|
|
21
|
+
|
|
22
|
+
- **历史来自会话自身的消息记录**:直接读取会话快照中的用户消息节点(`user` / `steering`),随消息落地实时追加——与聊天记录严格一致,随会话持久化,刷新页面后依然可用,不需要任何配置或额外存储
|
|
23
|
+
- **连续重复自动合并**,浏览状态跟随会话切换自动重置
|
|
24
|
+
- 无 UI、无设置项,bundle 仅约 7 KB;依赖全部走官方 `@deepseek-ai/*` peer 依赖
|
|
25
|
+
|
|
26
|
+
## 已知限制
|
|
27
|
+
|
|
28
|
+
- 切换会话时,只能召回当前已加载事件窗口内的旧消息(页面上新发送的都在);不做宿主侧历史拉取
|
|
29
|
+
- 纯文本召回:带图片/命令 chip 的消息不参与;召回内容为纯文本
|
|
30
|
+
|
|
31
|
+
## 开发
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pnpm install
|
|
35
|
+
pnpm run typecheck # tsc --noEmit
|
|
36
|
+
pnpm run build # tsc(lib/types)+ tsdown(lib/index.js / lib/invariant.js / lib/client.js)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
浏览器半区(`src/client/`)注册在 `conversation.input.right` 槽位,构建产物为 DSH `__ModuleLoader__` 闭包格式,外部依赖仅 `react`(其余由浏览器模块表提供)。
|
|
40
|
+
|
|
41
|
+
## 原理
|
|
42
|
+
|
|
43
|
+
插件是一个不可见的 composer 槽位条目:挂一个 document 捕获期 keydown 监听,仅在目标为 composer 输入框、无修饰键、非输入法组合、菜单未打开、会话非忙碌时接管 ↑/↓,通过 `inputActions.setDraft` 写入历史文本。历史列表由会话快照的 `user`/`steering` 节点按 seq 去重追加,浏览位置(index + 待恢复的当前行)保存在组件 ref 中。
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
[MIT](LICENSE)
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# dsh-prompt-history
|
|
2
|
+
|
|
3
|
+
Bash-like prompt history for the DSH web composer: **Up / Down** arrows recall previously submitted messages in the input box.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
dsh plugin --profile web add dsh-prompt-history
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then refresh the page.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
- **Up**: recall the previous submitted prompt (newest first), replacing the current draft; keep pressing Up to walk further back.
|
|
16
|
+
- **Down**: walk forward; at the bottom edge, restore the line you were typing before browsing began (readline pending-line behavior).
|
|
17
|
+
- **Edit exits browsing**: editing the draft while browsing drops back to the live line.
|
|
18
|
+
- **Does not interfere**: while the `/` or `@` suggestion menu is open, arrow keys stay with menu navigation; IME composition, `Shift+Up` selection, `Ctrl+Up` word-jumps and other modifier chords are never intercepted; busy/removed sessions do not respond.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **History comes from the session's own message log**: reads the conversation snapshot's user nodes (`user` / `steering`) and appends as they land — strictly consistent with the transcript, persisted with the session, survives page reloads, and needs no configuration or extra storage.
|
|
23
|
+
- **Consecutive duplicates collapse**; browse state resets on session switch.
|
|
24
|
+
- No UI, no settings; the client bundle is ~7 KB and depends only on the official `@deepseek-ai/*` peer packages.
|
|
25
|
+
|
|
26
|
+
## Known limitations
|
|
27
|
+
|
|
28
|
+
- On a session switch, only the currently loaded event window's messages are recallable (everything submitted while the page is open is included); there is no host-side history fetch.
|
|
29
|
+
- Plain text only: image-only or chip-bearing messages are not recalled; recalled drafts are plain text.
|
|
30
|
+
|
|
31
|
+
## Development
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pnpm install
|
|
35
|
+
pnpm run typecheck # tsc --noEmit
|
|
36
|
+
pnpm run build # tsc (lib/types) + tsdown (lib/index.js / lib/invariant.js / lib/client.js)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The browser half (`src/client/`) registers into the `conversation.input.right` slot; the build emits the DSH `__ModuleLoader__` closure format with `react` as the only external (everything else comes from the browser module table).
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
The plugin is an invisible composer slot entry that mounts a capture-phase `keydown` listener on the document. It takes over Up/Down only when the target is the composer textarea, no modifiers are held, no IME composition is active, no suggestion menu is open, and the session is not busy — then writes the recalled text through `inputActions.setDraft`. The history list is fed from the snapshot's `user`/`steering` nodes (deduped by seq); the browse position (index plus the pending line to restore) lives in component refs.
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# dsh-prompt-history bundle patch — bash-like prompt history for the composer.
|
|
2
|
+
#
|
|
3
|
+
# Mounts the browser half by inserting one row into the dsh.client roster; the
|
|
4
|
+
# loader imports the package root (empty node half) and the modules node half
|
|
5
|
+
# serves exports["./client"] to the browser.
|
|
6
|
+
- insert:
|
|
7
|
+
- id: dsh-prompt-history
|
|
8
|
+
name: dsh-prompt-history
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-prompt-history",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
//#region src/client/InputHistory.tsx
|
|
9
|
+
/**
|
|
10
|
+
* InputHistory: bash-like prompt history for the composer (dsh-prompt-history).
|
|
11
|
+
* Renders nothing — it mounts one capture-phase keydown listener on the
|
|
12
|
+
* document while the session's composer card is live. Up recalls previously
|
|
13
|
+
* submitted prompts (newest first), Down walks forward and restores the line
|
|
14
|
+
* that was being typed before browsing began; editing the draft while browsing
|
|
15
|
+
* drops back to the live line. The '/' and '@' suggestion menus keep their own
|
|
16
|
+
* arrow-key navigation: while the menu (role=listbox inside the composer card)
|
|
17
|
+
* is open, this listener declines and the input trigger pipeline owns the keys.
|
|
18
|
+
*
|
|
19
|
+
* History is fed from the conversation snapshot's user nodes (kind 'user' and
|
|
20
|
+
* 'steering'), appended as they land — so everything submitted while the page
|
|
21
|
+
* is open is recallable even after the session event window slides. Consecutive
|
|
22
|
+
* duplicates collapse; on a session switch the window's in-window user messages
|
|
23
|
+
* seed the list. Unlike a parallel storage ring, the history IS the session's
|
|
24
|
+
* own message log: it stays consistent with the transcript, survives reloads
|
|
25
|
+
* through the session itself, and needs no configuration or extra storage.
|
|
26
|
+
*
|
|
27
|
+
* Interception guards: composer textarea target only, no modifier keys
|
|
28
|
+
* (Shift+Up still extends selection), no IME composition, machine not
|
|
29
|
+
* adjudicating/submitting, session not removed, and a non-empty history.
|
|
30
|
+
*/
|
|
31
|
+
/** Not-browsing state; also the reset target after edits and session switches. */
|
|
32
|
+
const RESET_BROWSE = {
|
|
33
|
+
index: -1,
|
|
34
|
+
saved: "",
|
|
35
|
+
lastSet: null
|
|
36
|
+
};
|
|
37
|
+
/** The composer card's own textarea is the only interception target. */
|
|
38
|
+
const COMPOSER_CARD = "[data-composer-card]";
|
|
39
|
+
/** The suggestion menu (slash/at) renders a listbox inside the card while open. */
|
|
40
|
+
const OPEN_MENU = "[role=\"listbox\"]";
|
|
41
|
+
/** Extract the trimmed plain text of one user-submitted message node; null when empty. */
|
|
42
|
+
function promptText(node) {
|
|
43
|
+
if (node.kind !== "user" && node.kind !== "steering") return null;
|
|
44
|
+
let text = "";
|
|
45
|
+
for (const block of node.content) if (block.type === "text") text += block.text;
|
|
46
|
+
const trimmed = text.trim();
|
|
47
|
+
return trimmed === "" ? null : trimmed;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The composer history entry: no visible UI, one keydown listener.
|
|
51
|
+
* @param props - framework standard kit (useInput/useSession/inputActions/sessionId).
|
|
52
|
+
* @returns null (the entry is invisible chrome).
|
|
53
|
+
*/
|
|
54
|
+
function InputHistory({ useInput, useSession, inputActions, sessionId }) {
|
|
55
|
+
const draft = useInput((s) => s.draft);
|
|
56
|
+
const phase = useInput((s) => s.phase);
|
|
57
|
+
const nodes = useSession((s) => s.nodes);
|
|
58
|
+
const removed = useSession((s) => s.removed) ?? false;
|
|
59
|
+
/** Submitted prompt texts, oldest → newest (per session; append-only). */
|
|
60
|
+
const historyRef = (0, react.useRef)([]);
|
|
61
|
+
/** User-node seqs already folded into historyRef (append-once dedup). */
|
|
62
|
+
const seenRef = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
63
|
+
const browseRef = (0, react.useRef)(RESET_BROWSE);
|
|
64
|
+
const liveRef = (0, react.useRef)({
|
|
65
|
+
draft,
|
|
66
|
+
phase,
|
|
67
|
+
removed,
|
|
68
|
+
inputActions
|
|
69
|
+
});
|
|
70
|
+
liveRef.current = {
|
|
71
|
+
draft,
|
|
72
|
+
phase,
|
|
73
|
+
removed,
|
|
74
|
+
inputActions
|
|
75
|
+
};
|
|
76
|
+
(0, react.useEffect)(() => {
|
|
77
|
+
historyRef.current = [];
|
|
78
|
+
seenRef.current = /* @__PURE__ */ new Set();
|
|
79
|
+
browseRef.current = RESET_BROWSE;
|
|
80
|
+
}, [sessionId]);
|
|
81
|
+
(0, react.useEffect)(() => {
|
|
82
|
+
const seen = seenRef.current;
|
|
83
|
+
const history = historyRef.current;
|
|
84
|
+
for (const node of nodes) {
|
|
85
|
+
if (node.kind !== "user" && node.kind !== "steering") continue;
|
|
86
|
+
if (seen.has(node.seq)) continue;
|
|
87
|
+
seen.add(node.seq);
|
|
88
|
+
const text = promptText(node);
|
|
89
|
+
if (text === null) continue;
|
|
90
|
+
if (history[history.length - 1] !== text) history.push(text);
|
|
91
|
+
}
|
|
92
|
+
}, [nodes]);
|
|
93
|
+
(0, react.useEffect)(() => {
|
|
94
|
+
const browse = browseRef.current;
|
|
95
|
+
if (browse.index !== -1 && draft !== browse.lastSet) browseRef.current = RESET_BROWSE;
|
|
96
|
+
}, [draft]);
|
|
97
|
+
(0, react.useEffect)(() => {
|
|
98
|
+
const onKeyDown = (e) => {
|
|
99
|
+
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
100
|
+
const target = e.target;
|
|
101
|
+
if (!(target instanceof HTMLTextAreaElement)) return;
|
|
102
|
+
const card = target.closest(COMPOSER_CARD);
|
|
103
|
+
if (card === null) return;
|
|
104
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
105
|
+
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
106
|
+
if (card.querySelector(OPEN_MENU) !== null) return;
|
|
107
|
+
const live = liveRef.current;
|
|
108
|
+
if (live.phase === "adjudicating" || live.phase === "submitting" || live.removed) return;
|
|
109
|
+
const history = historyRef.current;
|
|
110
|
+
if (history.length === 0) return;
|
|
111
|
+
const recall = (index) => history[index] ?? "";
|
|
112
|
+
const browse = browseRef.current;
|
|
113
|
+
if (e.key === "ArrowDown" && browse.index === -1) return;
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
if (e.key === "ArrowUp") {
|
|
117
|
+
if (browse.index === -1) {
|
|
118
|
+
const index = history.length - 1;
|
|
119
|
+
browseRef.current = {
|
|
120
|
+
index,
|
|
121
|
+
saved: live.draft,
|
|
122
|
+
lastSet: recall(index)
|
|
123
|
+
};
|
|
124
|
+
live.inputActions.setDraft(recall(index));
|
|
125
|
+
} else {
|
|
126
|
+
const index = Math.max(0, browse.index - 1);
|
|
127
|
+
browseRef.current = {
|
|
128
|
+
...browse,
|
|
129
|
+
index,
|
|
130
|
+
lastSet: recall(index)
|
|
131
|
+
};
|
|
132
|
+
live.inputActions.setDraft(recall(index));
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (browse.index + 1 >= history.length) {
|
|
137
|
+
browseRef.current = RESET_BROWSE;
|
|
138
|
+
live.inputActions.setDraft(browse.saved);
|
|
139
|
+
} else {
|
|
140
|
+
const index = browse.index + 1;
|
|
141
|
+
browseRef.current = {
|
|
142
|
+
...browse,
|
|
143
|
+
index,
|
|
144
|
+
lastSet: recall(index)
|
|
145
|
+
};
|
|
146
|
+
live.inputActions.setDraft(recall(index));
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
150
|
+
return () => {
|
|
151
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
152
|
+
};
|
|
153
|
+
}, []);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/client/index.ts
|
|
158
|
+
/** Required services: the slot registry the entry registers into. */
|
|
159
|
+
const inject = ["slots"];
|
|
160
|
+
/**
|
|
161
|
+
* Client plugin body: register the composer input-history entry. The
|
|
162
|
+
* declaration lives in ui-conversation, whose apply order is unconstrained —
|
|
163
|
+
* slots.inject waits on the actual declaration and retires the contribution
|
|
164
|
+
* with this plugin's fiber.
|
|
165
|
+
* @param ctx - client root context.
|
|
166
|
+
*/
|
|
167
|
+
function apply(ctx) {
|
|
168
|
+
ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|
|
169
|
+
name: "conversation.input.right",
|
|
170
|
+
id: "dsh-prompt-history"
|
|
171
|
+
}, InputHistory));
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
exports.apply = apply;
|
|
175
|
+
exports.inject = inject;
|
|
176
|
+
return module.exports;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region lib/types/index.js
|
|
2
|
+
/**
|
|
3
|
+
* dsh-prompt-history, node half. Pure UI plugin: the empty apply exists so
|
|
4
|
+
* the plugin appears in the host cordis.yml / Loader; the browser half ships
|
|
5
|
+
* via exports["./client"], discovered through the package.json dsh.client
|
|
6
|
+
* declaration.
|
|
7
|
+
*/
|
|
8
|
+
/** Host plugin body — no host-side behavior for this UI-only plugin. */
|
|
9
|
+
function apply() {}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { apply };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `dsh-prompt-history`.
|
|
4
|
+
* @module dsh-prompt-history/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "dsh-prompt-history";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "dsh-prompt-history-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: the composer slot registration is a registry-owned
|
|
13
|
+
* registration whose disposal is proven by the HMR-safety spec, and the
|
|
14
|
+
* per-session keydown listener is React-effect-owned (unmount tears it down).
|
|
15
|
+
* The plugin emits no cordis events and owns no cross-plugin mutable state.
|
|
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
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
24
|
+
//#endregion
|
|
25
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
/** Full props of the input-history entry: framework standard kit + owner share. */
|
|
3
|
+
export type InputHistoryProps = PropsRuntime<'conversation.input.right'>;
|
|
4
|
+
/**
|
|
5
|
+
* The composer history entry: no visible UI, one keydown listener.
|
|
6
|
+
* @param props - framework standard kit (useInput/useSession/inputActions/sessionId).
|
|
7
|
+
* @returns null (the entry is invisible chrome).
|
|
8
|
+
*/
|
|
9
|
+
export declare function InputHistory({ useInput, useSession, inputActions, sessionId }: InputHistoryProps): null;
|
|
10
|
+
//# sourceMappingURL=InputHistory.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-history plugin, browser half: one invisible entry in the composer's
|
|
3
|
+
* input.right tool row that mounts the capture-phase keyboard listener (see
|
|
4
|
+
* InputHistory.tsx for the full behavior contract).
|
|
5
|
+
*/
|
|
6
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
7
|
+
/** Required services: the slot registry the entry registers into. */
|
|
8
|
+
export declare const inject: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Client plugin body: register the composer input-history entry. The
|
|
11
|
+
* declaration lives in ui-conversation, whose apply order is unconstrained —
|
|
12
|
+
* slots.inject waits on the actual declaration and retires the contribution
|
|
13
|
+
* with this plugin's fiber.
|
|
14
|
+
* @param ctx - client root context.
|
|
15
|
+
*/
|
|
16
|
+
export declare function apply(ctx: ClientContext): void;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { InputHistory } from "./InputHistory.js";
|
|
2
|
+
/** Required services: the slot registry the entry registers into. */
|
|
3
|
+
export const inject = ['slots'];
|
|
4
|
+
/**
|
|
5
|
+
* Client plugin body: register the composer input-history entry. The
|
|
6
|
+
* declaration lives in ui-conversation, whose apply order is unconstrained —
|
|
7
|
+
* slots.inject waits on the actual declaration and retires the contribution
|
|
8
|
+
* with this plugin's fiber.
|
|
9
|
+
* @param ctx - client root context.
|
|
10
|
+
*/
|
|
11
|
+
export function apply(ctx) {
|
|
12
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({ name: 'conversation.input.right', id: 'dsh-prompt-history' }, InputHistory));
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-prompt-history, node half. Pure UI plugin: the empty apply exists so
|
|
3
|
+
* the plugin appears in the host cordis.yml / Loader; the browser half ships
|
|
4
|
+
* via exports["./client"], discovered through the package.json dsh.client
|
|
5
|
+
* declaration.
|
|
6
|
+
*/
|
|
7
|
+
/** Host plugin body — no host-side behavior for this UI-only plugin. */
|
|
8
|
+
export declare function apply(): void;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-prompt-history, node half. Pure UI plugin: the empty apply exists so
|
|
3
|
+
* the plugin appears in the host cordis.yml / Loader; the browser half ships
|
|
4
|
+
* via exports["./client"], discovered through the package.json dsh.client
|
|
5
|
+
* declaration.
|
|
6
|
+
*/
|
|
7
|
+
/** Host plugin body — no host-side behavior for this UI-only plugin. */
|
|
8
|
+
export function apply() { }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-prompt-history`.
|
|
3
|
+
* @module dsh-prompt-history/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "dsh-prompt-history-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,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-prompt-history`.
|
|
3
|
+
* @module dsh-prompt-history/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = 'dsh-prompt-history';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'dsh-prompt-history-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: the composer slot registration is a registry-owned
|
|
12
|
+
* registration whose disposal is proven by the HMR-safety spec, and the
|
|
13
|
+
* per-session keydown listener is React-effect-owned (unmount tears it down).
|
|
14
|
+
* The plugin emits no cordis events and owns no cross-plugin mutable state.
|
|
15
|
+
*/
|
|
16
|
+
const install = () => { };
|
|
17
|
+
/**
|
|
18
|
+
* Register this package's invariant companion.
|
|
19
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
20
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
21
|
+
*/
|
|
22
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
23
|
+
/* jscpd:ignore-end */
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-prompt-history",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DSH web plugin: bash-like prompt history in the composer — Up/Down arrows recall previously submitted messages, sourced from the session's own message log (zero config, zero extra storage).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"plugin",
|
|
10
|
+
"history",
|
|
11
|
+
"composer",
|
|
12
|
+
"input",
|
|
13
|
+
"prompt"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/Xiaofei-fei/dsh-prompt-history"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"main": "lib/index.js",
|
|
24
|
+
"types": "lib/types/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./lib/types/index.d.ts",
|
|
28
|
+
"default": "./lib/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./invariant": {
|
|
31
|
+
"types": "./lib/types/invariant.d.ts",
|
|
32
|
+
"default": "./lib/invariant.js"
|
|
33
|
+
},
|
|
34
|
+
"./client": {
|
|
35
|
+
"types": "./lib/types/client/index.d.ts",
|
|
36
|
+
"default": "./lib/client.js"
|
|
37
|
+
},
|
|
38
|
+
"./src/*": "./src/*",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"dsh": {
|
|
45
|
+
"bundle": {
|
|
46
|
+
"patch": "./cordis.patch.yml"
|
|
47
|
+
},
|
|
48
|
+
"client": {
|
|
49
|
+
"inject": [
|
|
50
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
52
|
+
],
|
|
53
|
+
"platform": "web"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"lib/index.js",
|
|
58
|
+
"lib/invariant.js",
|
|
59
|
+
"lib/client.js",
|
|
60
|
+
"lib/types/**/*.d.ts",
|
|
61
|
+
"src",
|
|
62
|
+
"cordis.patch.yml",
|
|
63
|
+
"README.md",
|
|
64
|
+
"README_EN.md",
|
|
65
|
+
"LICENSE"
|
|
66
|
+
],
|
|
67
|
+
"license": "MIT",
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
70
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
73
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
74
|
+
"react": "^18.2.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
78
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.7",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.7",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.7",
|
|
81
|
+
"@deepseek-ai/dsh-invariants": "0.1.0-rc.7",
|
|
82
|
+
"@types/react": "~18.3.1",
|
|
83
|
+
"react": "^18.2.0",
|
|
84
|
+
"tsdown": "^0.22.2",
|
|
85
|
+
"typescript": "^5.6.0",
|
|
86
|
+
"unrun": "^0.3.1"
|
|
87
|
+
},
|
|
88
|
+
"scripts": {
|
|
89
|
+
"build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
|
|
90
|
+
"prepare": "pnpm build",
|
|
91
|
+
"typecheck": "tsc --noEmit",
|
|
92
|
+
"bundle": "tsdown",
|
|
93
|
+
"watch": "tsdown --watch"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputHistory: bash-like prompt history for the composer (dsh-prompt-history).
|
|
3
|
+
* Renders nothing — it mounts one capture-phase keydown listener on the
|
|
4
|
+
* document while the session's composer card is live. Up recalls previously
|
|
5
|
+
* submitted prompts (newest first), Down walks forward and restores the line
|
|
6
|
+
* that was being typed before browsing began; editing the draft while browsing
|
|
7
|
+
* drops back to the live line. The '/' and '@' suggestion menus keep their own
|
|
8
|
+
* arrow-key navigation: while the menu (role=listbox inside the composer card)
|
|
9
|
+
* is open, this listener declines and the input trigger pipeline owns the keys.
|
|
10
|
+
*
|
|
11
|
+
* History is fed from the conversation snapshot's user nodes (kind 'user' and
|
|
12
|
+
* 'steering'), appended as they land — so everything submitted while the page
|
|
13
|
+
* is open is recallable even after the session event window slides. Consecutive
|
|
14
|
+
* duplicates collapse; on a session switch the window's in-window user messages
|
|
15
|
+
* seed the list. Unlike a parallel storage ring, the history IS the session's
|
|
16
|
+
* own message log: it stays consistent with the transcript, survives reloads
|
|
17
|
+
* through the session itself, and needs no configuration or extra storage.
|
|
18
|
+
*
|
|
19
|
+
* Interception guards: composer textarea target only, no modifier keys
|
|
20
|
+
* (Shift+Up still extends selection), no IME composition, machine not
|
|
21
|
+
* adjudicating/submitting, session not removed, and a non-empty history.
|
|
22
|
+
*/
|
|
23
|
+
import { useEffect, useRef } from 'react'
|
|
24
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
25
|
+
// Type-only: the ui-conversation SlotMap merge (the input.right entry) and the
|
|
26
|
+
// session standard kit members (useInput/inputActions).
|
|
27
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
28
|
+
// Type-only: the runtime SessionStandardProps merge (useSession/sessionId) and
|
|
29
|
+
// the conversation node union.
|
|
30
|
+
import type { ConversationNode } from '@deepseek-ai/dsh-client-runtime/client'
|
|
31
|
+
|
|
32
|
+
/** Full props of the input-history entry: framework standard kit + owner share. */
|
|
33
|
+
export type InputHistoryProps = PropsRuntime<'conversation.input.right'>
|
|
34
|
+
|
|
35
|
+
/** One browse-position snapshot; index -1 = showing the live draft (not browsing). */
|
|
36
|
+
interface BrowseState {
|
|
37
|
+
/** History index currently shown; -1 = the live line. */
|
|
38
|
+
readonly index: number
|
|
39
|
+
/** The draft saved when browsing started; restored at the bottom edge. */
|
|
40
|
+
readonly saved: string
|
|
41
|
+
/** The exact draft our own setDraft last wrote (user-edit detection). */
|
|
42
|
+
readonly lastSet: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Not-browsing state; also the reset target after edits and session switches. */
|
|
46
|
+
const RESET_BROWSE: BrowseState = { index: -1, saved: '', lastSet: null }
|
|
47
|
+
|
|
48
|
+
/** The composer card's own textarea is the only interception target. */
|
|
49
|
+
const COMPOSER_CARD = '[data-composer-card]'
|
|
50
|
+
/** The suggestion menu (slash/at) renders a listbox inside the card while open. */
|
|
51
|
+
const OPEN_MENU = '[role="listbox"]'
|
|
52
|
+
|
|
53
|
+
/** Extract the trimmed plain text of one user-submitted message node; null when empty. */
|
|
54
|
+
function promptText(node: ConversationNode): string | null {
|
|
55
|
+
if (node.kind !== 'user' && node.kind !== 'steering') return null
|
|
56
|
+
let text = ''
|
|
57
|
+
for (const block of node.content) {
|
|
58
|
+
if (block.type === 'text') text += block.text
|
|
59
|
+
}
|
|
60
|
+
const trimmed = text.trim()
|
|
61
|
+
return trimmed === '' ? null : trimmed
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The composer history entry: no visible UI, one keydown listener.
|
|
66
|
+
* @param props - framework standard kit (useInput/useSession/inputActions/sessionId).
|
|
67
|
+
* @returns null (the entry is invisible chrome).
|
|
68
|
+
*/
|
|
69
|
+
export function InputHistory({ useInput, useSession, inputActions, sessionId }: InputHistoryProps) {
|
|
70
|
+
// Latest machine/session facts at event time (the listener mounts once).
|
|
71
|
+
const draft = useInput(s => s.draft)
|
|
72
|
+
const phase = useInput(s => s.phase)
|
|
73
|
+
const nodes = useSession(s => s.nodes)
|
|
74
|
+
const removed = useSession(s => s.removed) ?? false
|
|
75
|
+
|
|
76
|
+
/** Submitted prompt texts, oldest → newest (per session; append-only). */
|
|
77
|
+
const historyRef = useRef<string[]>([])
|
|
78
|
+
/** User-node seqs already folded into historyRef (append-once dedup). */
|
|
79
|
+
const seenRef = useRef<Set<number>>(new Set())
|
|
80
|
+
const browseRef = useRef<BrowseState>(RESET_BROWSE)
|
|
81
|
+
const liveRef = useRef({ draft, phase, removed, inputActions })
|
|
82
|
+
liveRef.current = { draft, phase, removed, inputActions }
|
|
83
|
+
|
|
84
|
+
// Session switch: start a fresh history, seen set, and browse position.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
historyRef.current = []
|
|
87
|
+
seenRef.current = new Set()
|
|
88
|
+
browseRef.current = RESET_BROWSE
|
|
89
|
+
}, [sessionId])
|
|
90
|
+
|
|
91
|
+
// Fold newly arrived user messages into the history (window slides; the
|
|
92
|
+
// append-only list survives it).
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const seen = seenRef.current
|
|
95
|
+
const history = historyRef.current
|
|
96
|
+
for (const node of nodes) {
|
|
97
|
+
if (node.kind !== 'user' && node.kind !== 'steering') continue
|
|
98
|
+
if (seen.has(node.seq)) continue
|
|
99
|
+
seen.add(node.seq)
|
|
100
|
+
const text = promptText(node)
|
|
101
|
+
if (text === null) continue
|
|
102
|
+
if (history[history.length - 1] !== text) history.push(text)
|
|
103
|
+
}
|
|
104
|
+
}, [nodes])
|
|
105
|
+
|
|
106
|
+
// Any draft change that is not our own history write ends the browse
|
|
107
|
+
// session (bash drops the recalled line when you edit it).
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const browse = browseRef.current
|
|
110
|
+
if (browse.index !== -1 && draft !== browse.lastSet) browseRef.current = RESET_BROWSE
|
|
111
|
+
}, [draft])
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const onKeyDown = (e: KeyboardEvent): void => {
|
|
115
|
+
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
|
116
|
+
const target = e.target
|
|
117
|
+
if (!(target instanceof HTMLTextAreaElement)) return
|
|
118
|
+
const card = target.closest(COMPOSER_CARD)
|
|
119
|
+
if (card === null) return
|
|
120
|
+
// IME composition and modifier chords stay native (Shift+Up selects text,
|
|
121
|
+
// Ctrl+Up moves by word).
|
|
122
|
+
if (e.isComposing || e.keyCode === 229) return
|
|
123
|
+
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return
|
|
124
|
+
// The suggestion menu owns the arrows while it is open.
|
|
125
|
+
if (card.querySelector(OPEN_MENU) !== null) return
|
|
126
|
+
const live = liveRef.current
|
|
127
|
+
if (live.phase === 'adjudicating' || live.phase === 'submitting' || live.removed) return
|
|
128
|
+
const history = historyRef.current
|
|
129
|
+
if (history.length === 0) return
|
|
130
|
+
// The browse indexes below are always in bounds (derived from the length
|
|
131
|
+
// or a clamped prior index), so the undefined arm of the indexed read is
|
|
132
|
+
// unreachable; the fallback keeps the write and the lastSet echo aligned.
|
|
133
|
+
const recall = (index: number): string => history[index] ?? ''
|
|
134
|
+
const browse = browseRef.current
|
|
135
|
+
|
|
136
|
+
if (e.key === 'ArrowDown' && browse.index === -1) return // native caret-down on the live line
|
|
137
|
+
|
|
138
|
+
e.preventDefault()
|
|
139
|
+
e.stopPropagation()
|
|
140
|
+
|
|
141
|
+
if (e.key === 'ArrowUp') {
|
|
142
|
+
if (browse.index === -1) {
|
|
143
|
+
// Enter browsing: save the live line, recall the newest prompt.
|
|
144
|
+
const index = history.length - 1
|
|
145
|
+
browseRef.current = { index, saved: live.draft, lastSet: recall(index) }
|
|
146
|
+
live.inputActions.setDraft(recall(index))
|
|
147
|
+
} else {
|
|
148
|
+
const index = Math.max(0, browse.index - 1)
|
|
149
|
+
browseRef.current = { ...browse, index, lastSet: recall(index) }
|
|
150
|
+
live.inputActions.setDraft(recall(index))
|
|
151
|
+
}
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ArrowDown while browsing.
|
|
156
|
+
if (browse.index + 1 >= history.length) {
|
|
157
|
+
// Bottom edge: restore the saved live line and stop browsing.
|
|
158
|
+
browseRef.current = RESET_BROWSE
|
|
159
|
+
live.inputActions.setDraft(browse.saved)
|
|
160
|
+
} else {
|
|
161
|
+
const index = browse.index + 1
|
|
162
|
+
browseRef.current = { ...browse, index, lastSet: recall(index) }
|
|
163
|
+
live.inputActions.setDraft(recall(index))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
167
|
+
return () => { document.removeEventListener('keydown', onKeyDown, true) }
|
|
168
|
+
}, [])
|
|
169
|
+
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-history plugin, browser half: one invisible entry in the composer's
|
|
3
|
+
* input.right tool row that mounts the capture-phase keyboard listener (see
|
|
4
|
+
* InputHistory.tsx for the full behavior contract).
|
|
5
|
+
*/
|
|
6
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
7
|
+
// Type-only: pulls the ui-conversation SlotMap merge (the input.right entry)
|
|
8
|
+
// and the session standard kit members used by the component.
|
|
9
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
10
|
+
import { InputHistory } from './InputHistory.tsx'
|
|
11
|
+
|
|
12
|
+
/** Required services: the slot registry the entry registers into. */
|
|
13
|
+
export const inject = ['slots']
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Client plugin body: register the composer input-history entry. The
|
|
17
|
+
* declaration lives in ui-conversation, whose apply order is unconstrained —
|
|
18
|
+
* slots.inject waits on the actual declaration and retires the contribution
|
|
19
|
+
* with this plugin's fiber.
|
|
20
|
+
* @param ctx - client root context.
|
|
21
|
+
*/
|
|
22
|
+
export function apply(ctx: ClientContext): void {
|
|
23
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
|
|
24
|
+
{ name: 'conversation.input.right', id: 'dsh-prompt-history' },
|
|
25
|
+
InputHistory,
|
|
26
|
+
))
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-prompt-history, node half. Pure UI plugin: the empty apply exists so
|
|
3
|
+
* the plugin appears in the host cordis.yml / Loader; the browser half ships
|
|
4
|
+
* via exports["./client"], discovered through the package.json dsh.client
|
|
5
|
+
* declaration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Host plugin body — no host-side behavior for this UI-only plugin. */
|
|
9
|
+
export function apply(): void {}
|
package/src/invariant.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-prompt-history`.
|
|
3
|
+
* @module dsh-prompt-history/invariant
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* jscpd:ignore-start */
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
8
|
+
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = 'dsh-prompt-history'
|
|
11
|
+
|
|
12
|
+
/** Cordis companion plugin name. */
|
|
13
|
+
export const name = 'dsh-prompt-history-invariant'
|
|
14
|
+
/** Service required before the companion can reserve package ownership. */
|
|
15
|
+
export const inject = ['invariants']
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* No runtime invariant: the composer slot registration is a registry-owned
|
|
19
|
+
* registration whose disposal is proven by the HMR-safety spec, and the
|
|
20
|
+
* per-session keydown listener is React-effect-owned (unmount tears it down).
|
|
21
|
+
* The plugin emits no cordis events and owns no cross-plugin mutable state.
|
|
22
|
+
*/
|
|
23
|
+
const install: InvariantInstaller = () => {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register this package's invariant companion.
|
|
27
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
28
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
29
|
+
*/
|
|
30
|
+
export const apply = (ctx: Context): Promise<() => void> =>
|
|
31
|
+
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
|
32
|
+
/* jscpd:ignore-end */
|