@huanlin/dsh-plugin-input-history 0.1.2 → 0.3.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.
@@ -1,170 +1,62 @@
1
- /**
2
- * dsh-plugin-input-history — browser half.
3
- *
4
- * Two registrations:
5
- * - `conversation.composer.dock` list slot (id `dsh-plugin-input-history`,
6
- * order 100) renders an invisible anchor that collects history from
7
- * `session.nodes` every render. The dock is session-scoped; DSH treats
8
- * blank sessions as "hero" and suppresses the dock, so history
9
- * collection only runs in active sessions. That is fine: the first
10
- * message in a blank session is collected after the session becomes
11
- * active (the message makes it non-blank).
12
- * - A document-level `keydown` listener attached in `apply` (NOT in the
13
- * dock component) this ensures the listener is always active,
14
- * including in hero/blank mode where the dock is suppressed. The
15
- * listener uses the native `value` setter + `dispatchEvent('input')`
16
- * to feed history text into the textarea, which triggers InputBar's
17
- * `onChange` `keyboard.setDraft` — the same path the user's typing
18
- * takes.
19
- *
20
- * History is collected from `user` and `steering` conversation nodes as
21
- * they appear in any session's `ConversationSnapshot`, persisted to
22
- * `localStorage` (FIFO, 500 entries), and shared across all sessions
23
- * in the same browser profile.
24
- *
25
- * @module @huanlin/dsh-plugin-input-history/client
26
- */
27
- import { HistoryDock, getHistoryStore } from "./HistoryDock.js";
28
- import { isImeComposition } from "./ime.js";
29
- import { cursorLineInfo, findComposerTextarea } from "./dom.js";
30
- import { nextIndex, entryAt } from "./history.js";
31
- import { en, NS, zh } from "./locales.js";
32
- import { dicts } from "./dictionaries.js";
33
- /** Required services: slots + locale. */
34
- export const inject = ['slots', 'locale'];
35
- /**
36
- * Navigation cursor + saved draft for the keydown listener. Module-scoped
37
- * because the listener is attached once in `apply` and must persist across
38
- * dock mount/unmount cycles.
39
- */
40
- let navCursor = null;
41
- let savedDraft = null;
42
- /**
43
- * Client plugin body: register the dock + attach the keydown listener.
44
- *
45
- * @param ctx - client root context.
46
- */
47
- export function apply(ctx) {
48
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-input-history: dictionaries');
49
- // better-locale override: register the 19-language dicts so a selected
50
- // override language (with DSH on 'en') replaces the plugin's copy. The
51
- // service is optional — no better-locale, no dicts.
52
- // Activation-order-safe: re-check ctx.get('betterLocale') on every locale
53
- // revision bump (better-locale bumps on activation + override switch).
54
- ctx.effect(() => {
55
- let dispose;
56
- const sync = () => {
57
- dispose?.();
58
- dispose = undefined;
59
- const store = ctx.get('betterLocale');
60
- if (store !== undefined) {
61
- dispose = store.register(NS, dicts);
62
- }
63
- };
64
- sync();
65
- const unsubscribe = ctx.locale.subscribe(sync);
66
- return () => {
67
- unsubscribe();
68
- dispose?.();
69
- };
70
- }, 'dsh-plugin-input-history: better-locale override dicts');
71
- // The dock collects history from session.nodes. It is session-scoped;
72
- // in hero/blank mode it is suppressed, but the keydown listener below
73
- // still works (it reads from the module-scope HistoryStore, which
74
- // persists across dock mount/unmount cycles via localStorage).
75
- ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
76
- name: 'conversation.composer.dock',
77
- id: 'dsh-plugin-input-history',
78
- order: 100,
79
- locale: NS,
80
- }, HistoryDock));
81
- // Attach the document-level keydown listener. This lives in `apply`
82
- // (not in the dock component) so it stays active even when the dock is
83
- // suppressed (hero/blank sessions — ConversationRoot.tsx:79-80 treats
84
- // blank sessions as hero, and line 156 skips the dock render).
85
- ctx.effect(() => {
86
- if (typeof document === 'undefined')
87
- return () => { };
88
- const handler = (event) => {
89
- if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
90
- return;
91
- if (isImeComposition(event))
92
- return;
93
- if (event.defaultPrevented)
94
- return;
95
- const textarea = findComposerTextarea(event.target);
96
- if (textarea === null)
97
- return;
98
- if (event.target !== textarea)
99
- return;
100
- // Skip if the textarea is readOnly or disabled — hero mode's
101
- // workspace-picker trigger, or a submitting machine phase.
102
- if (textarea.readOnly || textarea.disabled)
103
- return;
104
- const store = getHistoryStore();
105
- const history = store.list;
106
- // Multi-line boundary check.
107
- const info = cursorLineInfo(textarea.value, textarea.selectionStart, textarea.selectionEnd);
108
- if (event.key === 'ArrowUp' && !info.atFirstLine)
109
- return;
110
- if (event.key === 'ArrowDown' && !info.atLastLine)
111
- return;
112
- const dir = event.key === 'ArrowUp' ? 'up' : 'down';
113
- const next = nextIndex(navCursor, history.length, dir);
114
- // Down off the newest end: restore the saved draft (if any).
115
- if (next === null) {
116
- const saved = savedDraft;
117
- navCursor = null;
118
- if (saved !== null) {
119
- setNativeTextareaValue(textarea, saved);
120
- savedDraft = null;
121
- }
122
- event.preventDefault();
123
- return;
124
- }
125
- // Entering history: save the current draft the first time we
126
- // navigate away from "not navigating".
127
- if (navCursor === null && savedDraft === null) {
128
- savedDraft = textarea.value;
129
- }
130
- const entry = entryAt(history, next);
131
- if (entry === null)
132
- return;
133
- navCursor = next;
134
- setNativeTextareaValue(textarea, entry);
135
- event.preventDefault();
136
- };
137
- document.addEventListener('keydown', handler, false);
138
- return () => {
139
- document.removeEventListener('keydown', handler, false);
140
- };
141
- }, 'dsh-plugin-input-history: keydown listener');
142
- }
143
- /**
144
- * Set the textarea value via the native prototype setter and dispatch an
145
- * `input` event so React's controlled-component onChange fires.
146
- *
147
- * React 18 tracks the textarea's value internally; directly assigning
148
- * `textarea.value = x` does NOT trigger React's onChange because React's
149
- * value tracker compares against its last-seen value. Using the native
150
- * prototype setter bypasses React's tracker, and the dispatched `input`
151
- * event makes React detect the change and run InputBar's `onChange` →
152
- * `keyboard.setDraft(next)`. This is the same technique used by
153
- * browser automation libraries (Playwright, Testing Library) to simulate
154
- * user typing in React controlled inputs.
155
- *
156
- * @param textarea - the target textarea element.
157
- * @param value - the new value to set.
158
- */
159
- function setNativeTextareaValue(textarea, value) {
160
- const proto = window.HTMLTextAreaElement.prototype;
161
- const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
162
- if (descriptor === undefined || descriptor.set === undefined) {
163
- // Fallback: direct assignment (may not trigger React onChange in
164
- // all browsers, but better than nothing).
165
- textarea.value = value;
166
- return;
167
- }
168
- descriptor.set.call(textarea, value);
169
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
170
- }
1
+ /**
2
+ * dsh-plugin-input-history — browser half.
3
+ *
4
+ * One registration: the `conversation.composer.dock` list slot (id
5
+ * `dsh-plugin-input-history`, order 100) mounts the invisible dock entry
6
+ * that owns both plugin behaviors prompt-history collection from the
7
+ * Chat target's user/steering nodes, and the capture-phase document
8
+ * keydown listener that navigates the composer draft through
9
+ * `inputActions.setDraft`. See [HistoryDock.tsx](./HistoryDock.tsx) for
10
+ * the data flow; the dock is session-scoped, so in hero/blank mode the
11
+ * plugin is dormant (no input machine exists there to drive).
12
+ *
13
+ * History is collected from `user` and `steering` chat nodes of any active
14
+ * session, persisted to `localStorage` (FIFO, 500 entries), and shared
15
+ * across all sessions in the same browser profile.
16
+ *
17
+ * @module @huanlin/dsh-plugin-input-history/client
18
+ */
19
+ import { HistoryDock } from "./HistoryDock.js";
20
+ import { en, NS, zh } from "./locales.js";
21
+ import { dicts } from "./dictionaries.js";
22
+ /** Required services: slots + locale. */
23
+ export const inject = ['slots', 'locale'];
24
+ /**
25
+ * Client plugin body: register the dock + locale dictionaries.
26
+ *
27
+ * @param ctx - client root context.
28
+ */
29
+ export function apply(ctx) {
30
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-input-history: dictionaries');
31
+ // better-locale override: register the 19-language dicts so a selected
32
+ // override language (with DSH on 'en') replaces the plugin's copy. The
33
+ // service is optional no better-locale, no dicts.
34
+ // Activation-order-safe: re-check ctx.get('betterLocale') on every locale
35
+ // revision bump (better-locale bumps on activation + override switch).
36
+ ctx.effect(() => {
37
+ let dispose;
38
+ const sync = () => {
39
+ dispose?.();
40
+ dispose = undefined;
41
+ const store = ctx.get('betterLocale');
42
+ if (store !== undefined) {
43
+ dispose = store.register(NS, dicts);
44
+ }
45
+ };
46
+ sync();
47
+ const unsubscribe = ctx.locale.subscribe(sync);
48
+ return () => {
49
+ unsubscribe();
50
+ dispose?.();
51
+ };
52
+ }, 'dsh-plugin-input-history: better-locale override dicts');
53
+ // The dock collects history from the Chat target's nodes and attaches the
54
+ // capture-phase keydown listener. It is session-scoped; in hero/blank mode
55
+ // it is unmounted and the plugin is dormant (no input machine exists there).
56
+ ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
57
+ name: 'conversation.composer.dock',
58
+ id: 'dsh-plugin-input-history',
59
+ order: 100,
60
+ locale: NS,
61
+ }, HistoryDock));
62
+ }
@@ -1,25 +1,24 @@
1
- /**
2
- * dsh-plugin-input-history — host plugin entry.
3
- *
4
- * Client-only plugin: the host half has no runtime work. The browser half
5
- * (`./client`) registers an invisible entry in `conversation.composer.dock`
6
- * that attaches a document-level `keydown` listener and implements
7
- * terminal-style prompt history navigation (ArrowUp/ArrowDown) over the
8
- * composer textarea.
9
- *
10
- * History is collected from `user` and `steering` conversation nodes as
11
- * they appear in any session's `ConversationSnapshot`, persisted to
12
- * `localStorage` (FIFO, capacity 500), and shared across all sessions
13
- * in the same browser profile.
14
- *
15
- * @module @huanlin/dsh-plugin-input-history
16
- */
17
- import type { Context } from '@deepseek-ai/cordis';
18
- export declare const name = "dsh-plugin-input-history";
19
- export declare const inject: string[];
20
- /**
21
- * Host apply — no-op. The history navigation is a pure client-side UI
22
- * contribution; no host-side resources are used.
23
- * @param _ctx - host context (unused).
24
- */
25
- export declare function apply(_ctx: Context): void;
1
+ /**
2
+ * dsh-plugin-input-history — host plugin entry.
3
+ *
4
+ * Client-only plugin: the host half has no runtime work. The browser half
5
+ * (`./client`) registers an invisible entry in `conversation.composer.dock`
6
+ * that collects prompt history from the Chat target's user/steering nodes
7
+ * and implements terminal-style history navigation (ArrowUp/ArrowDown) over
8
+ * the composer's Lexical surface through `inputActions.setDraft`.
9
+ *
10
+ * History is collected from `user` and `steering` chat nodes of any active
11
+ * session, persisted to `localStorage` (FIFO, capacity 500), and shared
12
+ * across all sessions in the same browser profile.
13
+ *
14
+ * @module @huanlin/dsh-plugin-input-history
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ export declare const name = "dsh-plugin-input-history";
18
+ export declare const inject: string[];
19
+ /**
20
+ * Host apply — no-op. The history navigation is a pure client-side UI
21
+ * contribution; no host-side resources are used.
22
+ * @param _ctx - host context (unused).
23
+ */
24
+ export declare function apply(_ctx: Context): void;
@@ -1,26 +1,25 @@
1
- /**
2
- * dsh-plugin-input-history — host plugin entry.
3
- *
4
- * Client-only plugin: the host half has no runtime work. The browser half
5
- * (`./client`) registers an invisible entry in `conversation.composer.dock`
6
- * that attaches a document-level `keydown` listener and implements
7
- * terminal-style prompt history navigation (ArrowUp/ArrowDown) over the
8
- * composer textarea.
9
- *
10
- * History is collected from `user` and `steering` conversation nodes as
11
- * they appear in any session's `ConversationSnapshot`, persisted to
12
- * `localStorage` (FIFO, capacity 500), and shared across all sessions
13
- * in the same browser profile.
14
- *
15
- * @module @huanlin/dsh-plugin-input-history
16
- */
17
- export const name = 'dsh-plugin-input-history';
18
- export const inject = [];
19
- /**
20
- * Host apply — no-op. The history navigation is a pure client-side UI
21
- * contribution; no host-side resources are used.
22
- * @param _ctx - host context (unused).
23
- */
24
- export function apply(_ctx) {
25
- // Client-only plugin: all work happens in src/client/index.ts.
26
- }
1
+ /**
2
+ * dsh-plugin-input-history — host plugin entry.
3
+ *
4
+ * Client-only plugin: the host half has no runtime work. The browser half
5
+ * (`./client`) registers an invisible entry in `conversation.composer.dock`
6
+ * that collects prompt history from the Chat target's user/steering nodes
7
+ * and implements terminal-style history navigation (ArrowUp/ArrowDown) over
8
+ * the composer's Lexical surface through `inputActions.setDraft`.
9
+ *
10
+ * History is collected from `user` and `steering` chat nodes of any active
11
+ * session, persisted to `localStorage` (FIFO, capacity 500), and shared
12
+ * across all sessions in the same browser profile.
13
+ *
14
+ * @module @huanlin/dsh-plugin-input-history
15
+ */
16
+ export const name = 'dsh-plugin-input-history';
17
+ export const inject = [];
18
+ /**
19
+ * Host apply — no-op. The history navigation is a pure client-side UI
20
+ * contribution; no host-side resources are used.
21
+ * @param _ctx - host context (unused).
22
+ */
23
+ export function apply(_ctx) {
24
+ // Client-only plugin: all work happens in src/client/index.ts.
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huanlin/dsh-plugin-input-history",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -17,10 +17,6 @@
17
17
  "types": "./lib/types/index.d.ts",
18
18
  "import": "./lib/index.js"
19
19
  },
20
- "./invariant": {
21
- "types": "./lib/types/invariant.d.ts",
22
- "import": "./lib/invariant.js"
23
- },
24
20
  "./client": {
25
21
  "types": "./lib/types/client/index.d.ts",
26
22
  "default": "./lib/client.js"
@@ -38,41 +34,42 @@
38
34
  },
39
35
  "client": {
40
36
  "inject": [
41
- "@deepseek-ai/dsh-client-runtime",
42
37
  "@deepseek-ai/dsh-client-locale",
43
38
  "@deepseek-ai/dsh-client-ui-slots",
44
- "@deepseek-ai/dsh-client-ui-conversation"
39
+ "@deepseek-ai/dsh-client-ui-conversation",
40
+ "@deepseek-ai/dsh-client-ui-chat",
41
+ "@deepseek-ai/dsh-client-ui-renderer"
45
42
  ],
46
43
  "platform": "web"
47
44
  }
48
45
  },
49
46
  "peerDependencies": {
50
- "@deepseek-ai/dsh-client-locale": "^0.0.1-rc.1",
51
- "@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.1",
52
- "@deepseek-ai/dsh-client-ui-conversation": "^0.0.1-rc.1",
53
- "@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.1",
54
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
55
- "@deepseek-ai/cordis": "^4.0.1-rc.1",
47
+ "@deepseek-ai/cordis": "^4.0.1",
48
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
49
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
50
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.1",
51
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
52
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
56
53
  "react": "^18.2.0",
57
54
  "react-dom": "^18.2.0"
58
55
  },
59
56
  "peerDependenciesMeta": {
60
- "@deepseek-ai/dsh-client-locale": {
57
+ "@deepseek-ai/cordis": {
61
58
  "optional": true
62
59
  },
63
- "@deepseek-ai/dsh-client-runtime": {
60
+ "@deepseek-ai/dsh-client-locale": {
64
61
  "optional": true
65
62
  },
66
- "@deepseek-ai/dsh-client-ui-conversation": {
63
+ "@deepseek-ai/dsh-client-ui-chat": {
67
64
  "optional": true
68
65
  },
69
- "@deepseek-ai/dsh-client-ui-slots": {
66
+ "@deepseek-ai/dsh-client-ui-conversation": {
70
67
  "optional": true
71
68
  },
72
- "@deepseek-ai/dsh-invariants": {
69
+ "@deepseek-ai/dsh-client-ui-renderer": {
73
70
  "optional": true
74
71
  },
75
- "@deepseek-ai/cordis": {
72
+ "@deepseek-ai/dsh-client-ui-slots": {
76
73
  "optional": true
77
74
  },
78
75
  "react": {
package/lib/invariant.js DELETED
@@ -1,24 +0,0 @@
1
- //#region src/invariant.ts
2
- const PACKAGE_NAME = "@huanlin/dsh-plugin-input-history";
3
- /** Cordis companion plugin name. */
4
- const name = "dsh-plugin-input-history-invariant";
5
- /** Service required before the companion can reserve package ownership. */
6
- const inject = ["invariants"];
7
- /**
8
- * No runtime invariant: the single `conversation.composer.dock` slot
9
- * registration is a registry-owned contribution whose disposal is proven
10
- * by the HMR-safety spec. The plugin's only mutable state is the
11
- * localStorage-backed history array, whose lifecycle is bounded by the
12
- * browser profile (not the cordis fiber) and whose write path is
13
- * last-writer-wins with try/catch containment.
14
- */
15
- const install = () => {};
16
- /**
17
- * Register this package's invariant companion.
18
- * @param ctx - Cordis context carrying the invariant service.
19
- * @returns the installed registration's disposer after setup succeeds.
20
- */
21
- const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
-
23
- //#endregion
24
- export { apply, inject, name };
@@ -1,16 +0,0 @@
1
- /**
2
- * Package-owned invariant companion for `@huanlin/dsh-plugin-input-history`.
3
- *
4
- * @module @huanlin/dsh-plugin-input-history/invariant
5
- */
6
- import type { Context } from '@deepseek-ai/cordis';
7
- /** Cordis companion plugin name. */
8
- export declare const name = "dsh-plugin-input-history-invariant";
9
- /** Service required before the companion can reserve package ownership. */
10
- export declare const inject: string[];
11
- /**
12
- * Register this package's invariant companion.
13
- * @param ctx - Cordis context carrying the invariant service.
14
- * @returns the installed registration's disposer after setup succeeds.
15
- */
16
- export declare const apply: (ctx: Context) => Promise<() => void>;
@@ -1,26 +0,0 @@
1
- /**
2
- * Package-owned invariant companion for `@huanlin/dsh-plugin-input-history`.
3
- *
4
- * @module @huanlin/dsh-plugin-input-history/invariant
5
- */
6
- const PACKAGE_NAME = '@huanlin/dsh-plugin-input-history';
7
- /** Cordis companion plugin name. */
8
- export const name = 'dsh-plugin-input-history-invariant';
9
- /** Service required before the companion can reserve package ownership. */
10
- export const inject = ['invariants'];
11
- /**
12
- * No runtime invariant: the single `conversation.composer.dock` slot
13
- * registration is a registry-owned contribution whose disposal is proven
14
- * by the HMR-safety spec. The plugin's only mutable state is the
15
- * localStorage-backed history array, whose lifecycle is bounded by the
16
- * browser profile (not the cordis fiber) and whose write path is
17
- * last-writer-wins with try/catch containment.
18
- */
19
- const install = () => { };
20
- /**
21
- * Register this package's invariant companion.
22
- * @param ctx - Cordis context carrying the invariant service.
23
- * @returns the installed registration's disposer after setup succeeds.
24
- */
25
- export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
26
- /* jscpd:ignore-end */