dsh-vscode-mode 0.11.0 → 0.13.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.
Files changed (76) hide show
  1. package/README.md +158 -111
  2. package/assets/shell/dsh-open.cs +65 -149
  3. package/assets/shell/dsh-open.mjs +241 -0
  4. package/assets/shell/dsh-open.ps1 +28 -47
  5. package/assets/shell/dsh-open.sh +25 -66
  6. package/lib/client.js +13362 -11806
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +4110 -2507
  9. package/lib/index.js.map +1 -1
  10. package/package.json +9 -8
  11. package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
  12. package/src/childEnv.ts +19 -0
  13. package/src/client/activePathStore.ts +28 -0
  14. package/src/client/addToConversation.ts +4 -2
  15. package/src/client/ai/inlineProvider.ts +1 -8
  16. package/src/client/commandBridge.ts +14 -42
  17. package/src/client/externalOpen.ts +40 -19
  18. package/src/client/index.ts +101 -18
  19. package/src/client/keybindings.ts +36 -73
  20. package/src/client/monaco/loader.ts +3 -1
  21. package/src/client/openFlow.ts +36 -28
  22. package/src/client/openReceipt.ts +64 -0
  23. package/src/client/panelCommands.ts +101 -0
  24. package/src/client/pdf/pdfLoader.ts +4 -2
  25. package/src/client/receiptState.ts +120 -0
  26. package/src/client/rpc.ts +30 -4
  27. package/src/client/shortcutRecorder.ts +161 -0
  28. package/src/client/shortcutsOfficial.ts +460 -0
  29. package/src/client/sidebar/panels/RulesPanel.ts +15 -11
  30. package/src/client/sidebarBridge.ts +7 -0
  31. package/src/client/ui/DiffBadge.ts +2 -1
  32. package/src/client/ui/EditorView.ts +246 -137
  33. package/src/client/ui/McpSettings.ts +10 -8
  34. package/src/client/ui/OfficialSideTab.ts +5 -2
  35. package/src/client/ui/QuickOpen.ts +16 -16
  36. package/src/client/ui/ShortcutSettings.ts +274 -0
  37. package/src/client/ui/commandCatalog.ts +39 -43
  38. package/src/client/useOpenReceipt.ts +128 -0
  39. package/src/compat.ts +5 -3
  40. package/src/dap/manager.ts +46 -13
  41. package/src/devForm.ts +252 -92
  42. package/src/externalHandoff.ts +93 -52
  43. package/src/fileOpenSettings.ts +4 -24
  44. package/src/hostImport.ts +19 -0
  45. package/src/httpGuard.ts +101 -0
  46. package/src/index.ts +106 -97
  47. package/src/integrate.ts +56 -50
  48. package/src/lsp/transport.ts +84 -127
  49. package/src/mcp.ts +58 -9
  50. package/src/mcpIsolation.ts +211 -102
  51. package/src/mcpProject.ts +220 -133
  52. package/src/mcpRuntime.ts +343 -0
  53. package/src/openBridge.ts +69 -0
  54. package/src/openInbox.ts +207 -0
  55. package/src/paths.ts +8 -5
  56. package/src/processTree.ts +63 -0
  57. package/src/reveal.ts +5 -3
  58. package/src/routes.ts +156 -154
  59. package/src/rpc.ts +14 -13
  60. package/src/rules.ts +100 -44
  61. package/src/shared/appUrl.ts +19 -0
  62. package/src/shared/externalOpen.ts +71 -0
  63. package/src/shared/integration.ts +2 -0
  64. package/src/shared/keybindings.ts +256 -0
  65. package/src/shared/mcp.ts +4 -1
  66. package/src/shared/rpc.ts +7 -4
  67. package/src/shared/rules.ts +9 -1
  68. package/src/shared/svn.ts +6 -8
  69. package/src/svnPlanTool.ts +51 -0
  70. package/src/unityBridge.ts +26 -3
  71. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
  72. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
  73. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
  74. package/unity/com.dsh.editor/README.md +16 -25
  75. package/unity/com.dsh.editor/package.json +2 -2
  76. package/src/client/ui/KeybindingsPanel.ts +0 -190
@@ -0,0 +1,101 @@
1
+ import type { SessionScope } from './sessionScope.js'
2
+
3
+ export type PanelAction = 'quickOpen' | 'searchInFiles'
4
+ export interface PanelRoute {
5
+ readScope(): SessionScope
6
+ canOpen(): boolean
7
+ open(): boolean
8
+ }
9
+ interface PendingPanel extends SessionScope { action: PanelAction; expires: number }
10
+ interface PanelHost { action: PanelAction; scope: SessionScope; run(): void }
11
+ let route: PanelRoute | null = null
12
+ const hosts = new Set<PanelHost>()
13
+ const storage = globalThis as typeof globalThis & { __edrvPanelCommand__?: PendingPanel }
14
+
15
+ /**
16
+ * Compare the command's session and workspace identity.
17
+ * @author ddj 2026年09月28号
18
+ * @param left Requested scope.
19
+ * @param right Mounted scope.
20
+ * @returns Whether both refer to the same session and known workspace.
21
+ */
22
+ function sameScope(left: SessionScope, right: SessionScope): boolean {
23
+ return Boolean(left.sessionId && left.sessionId === right.sessionId
24
+ && (!left.cwd || !right.cwd || left.cwd === right.cwd))
25
+ }
26
+
27
+ /**
28
+ * Find the most recently mounted matching command receiver.
29
+ * @author ddj 2026年09月28号
30
+ * @param action Panel action.
31
+ * @param scope Active session scope.
32
+ * @returns Receiver if mounted.
33
+ */
34
+ function panelHost(action: PanelAction, scope: SessionScope): PanelHost | undefined {
35
+ return [...hosts].reverse().find((host) => host.action === action && sameScope(scope, host.scope))
36
+ }
37
+
38
+ /**
39
+ * Install the existing sidebar route and current-session reader.
40
+ * @author ddj 2026年09月28号
41
+ * @param next Route capabilities; no side effects during availability checks.
42
+ * @returns Identity-safe disposal; pending intent survives plugin reload.
43
+ */
44
+ export function setPanelRoute(next: PanelRoute): () => void {
45
+ route = next
46
+ return () => { if (route === next) route = null }
47
+ }
48
+
49
+ /**
50
+ * Check that a command has a mounted receiver or a usable editor route.
51
+ * @author ddj 2026年09月28号
52
+ * @param action Panel action.
53
+ * @returns False when genuinely unavailable, allowing official key dispatch to pass.
54
+ */
55
+ export function panelAvailable(action: PanelAction): boolean {
56
+ const scope = route?.readScope()
57
+ if (!scope?.sessionId) return false
58
+ return Boolean(panelHost(action, scope) || route?.canOpen())
59
+ }
60
+
61
+ /**
62
+ * Deliver now or preserve an intent until the editor mounts.
63
+ * @author ddj 2026年09月28号
64
+ * @param action Panel action.
65
+ * @returns Whether the mounted host or sidebar accepted the request.
66
+ */
67
+ export function runPanelCommand(action: PanelAction): boolean {
68
+ const scope = route?.readScope()
69
+ if (!scope?.sessionId) return false
70
+ const host = panelHost(action, scope)
71
+ if (host) { host.run(); return true }
72
+ if (!route?.canOpen()) return false
73
+ const pending = { ...scope, action, expires: Date.now() + 30_000 }
74
+ storage.__edrvPanelCommand__ = pending
75
+ try {
76
+ if (route.open()) return true
77
+ } catch { /* A disappearing sidebar leaves the command unhandled. */ }
78
+ if (storage.__edrvPanelCommand__ === pending) delete storage.__edrvPanelCommand__
79
+ return false
80
+ }
81
+
82
+ /**
83
+ * Attach one scoped receiver and consume a still-current pending intent once.
84
+ * @author ddj 2026年09月28号
85
+ * @param action Panel action.
86
+ * @param scope Mounted session/workspace.
87
+ * @param run Action using the mounted component's state.
88
+ * @returns Receiver disposal.
89
+ */
90
+ export function bindPanelCommand(action: PanelAction, scope: SessionScope, run: () => void): () => void {
91
+ const host = { action, scope, run }
92
+ hosts.add(host)
93
+ const pending = storage.__edrvPanelCommand__
94
+ if (pending && (pending.expires < Date.now() || !sameScope(pending, route?.readScope() ?? {}))) {
95
+ delete storage.__edrvPanelCommand__
96
+ } else if (pending?.action === action && sameScope(pending, scope)) {
97
+ delete storage.__edrvPanelCommand__
98
+ run()
99
+ }
100
+ return () => { hosts.delete(host) }
101
+ }
@@ -7,8 +7,10 @@
7
7
  * 作者 ddj 2026-09-22
8
8
  */
9
9
 
10
- /** vendor 根(routes.ts /edrv/vendor 前缀路由分发 assets/vendor/*)。 */
11
- export const PDFJS_BASE = '/edrv/vendor/pdfjs'
10
+ import { appUrl } from '../../shared/appUrl.js'
11
+
12
+ /** vendor 根,保留官方桌面协议及反向代理前缀。 */
13
+ export const PDFJS_BASE = appUrl('/edrv/vendor/pdfjs')
12
14
  /** pdf.js Worker 地址(GlobalWorkerOptions.workerSrc)。 */
13
15
  export const PDFJS_WORKER_SRC = PDFJS_BASE + '/build/pdf.worker.mjs'
14
16
  /** CJK 字符映射目录(中文 PDF 渲染必需,尾斜杠固定)。 */
@@ -0,0 +1,120 @@
1
+ import { absoluteOf } from './tabActions.js'
2
+ import type { EditorRequest } from './openReceipt.js'
3
+
4
+ /** Identity of one load, committed together with its text or preview state. */
5
+ export interface ReceiptLoad {
6
+ sessionId: string
7
+ cwd: string | null
8
+ path: string
9
+ sequence: number
10
+ status: 'loading' | 'ready' | 'error'
11
+ error?: string
12
+ }
13
+
14
+ /** Current rendered facts; generic save/SVN errors intentionally have no receipt field. */
15
+ export interface ReceiptView {
16
+ sessionId: string
17
+ cwd?: string | null
18
+ active: string | null
19
+ sequence: number
20
+ load: ReceiptLoad | null
21
+ mounted: boolean
22
+ blocked: boolean
23
+ kind: 'text' | 'image' | 'pdf' | 'markdown'
24
+ contentPath: string | null
25
+ contentReady: boolean
26
+ modelMatches: boolean
27
+ positionMatches: boolean
28
+ imageLoaded: boolean
29
+ pdfLoaded: boolean
30
+ engineError?: string | null
31
+ }
32
+
33
+ /**
34
+ * Normalize identity using the workspace's path syntax, never the browser OS.
35
+ * @public @author ddj 2026年09月28号
36
+ * @param path File path.
37
+ * @param cwd Workspace root, including remote POSIX roots on Windows clients.
38
+ * @returns Absolute normalized identity, case folded only for Windows paths.
39
+ */
40
+ export function receiptPath(path: string, cwd?: string | null): string {
41
+ const value = absoluteOf(path, cwd)
42
+ return /^(?:[a-z]:\/|\/\/)/i.test(value) ? value.toLowerCase() : value
43
+ }
44
+
45
+ /**
46
+ * Preserve POSIX case when converting an external path into a tab path.
47
+ * @public @author ddj 2026年09月28号
48
+ * @param path External file path.
49
+ * @param cwd Workspace root.
50
+ * @returns Relative path only when it is inside the same correctly cased root.
51
+ */
52
+ export function receiptTabPath(path: string, cwd?: string | null): string {
53
+ const value = path.replace(/\\/g, '/')
54
+ const base = (cwd ?? '').replace(/\\/g, '/').replace(/\/+$/, '')
55
+ if (base && receiptPath(value).startsWith(receiptPath(base) + '/')) return value.slice(base.length + 1)
56
+ return value
57
+ }
58
+
59
+ /**
60
+ * Match a load to the current request, rendered path, and live load sequence.
61
+ * @private @author ddj 2026年09月28号
62
+ * @param request Claimed request.
63
+ * @param view Current render and load metadata.
64
+ * @returns Whether load facts belong to this exact target and generation.
65
+ */
66
+ function ownsLoad(request: EditorRequest, view: ReceiptView): boolean {
67
+ const load = view.load
68
+ if (!request.path || !view.active || !load) return false
69
+ const wanted = receiptPath(request.path, view.cwd)
70
+ return request.sessionId === view.sessionId && load.sessionId === view.sessionId
71
+ && load.sequence === view.sequence && receiptPath(load.path, load.cwd) === wanted
72
+ && receiptPath(view.active, view.cwd) === wanted
73
+ }
74
+
75
+ /**
76
+ * Confirm actual content/preview/navigation readiness for a correlated generation.
77
+ * @public @author ddj 2026年09月28号
78
+ * @param request Claimed external open.
79
+ * @param view Rendered state, including model identity and DOM readiness.
80
+ * @returns True only when the requested target is genuinely presented.
81
+ */
82
+ export function receiptReadyFor(request: EditorRequest, view: ReceiptView): boolean {
83
+ if (request.sessionId !== view.sessionId) return false
84
+ if (!request.path) return view.mounted
85
+ if (!ownsLoad(request, view) || view.load?.status !== 'ready' || view.blocked || view.engineError) return false
86
+ if (view.kind === 'image') return view.imageLoaded
87
+ if (view.kind === 'pdf') return view.pdfLoaded
88
+ if (!view.contentReady || !view.contentPath || receiptPath(view.contentPath, view.cwd) !== receiptPath(request.path, view.cwd)) return false
89
+ const positioned = request.line != null || request.column != null
90
+ if (view.kind === 'markdown') return !positioned
91
+ return view.modelMatches && (!positioned || view.positionMatches)
92
+ }
93
+
94
+ /**
95
+ * Return only errors from this target's current load/render generation.
96
+ * @public @author ddj 2026年09月28号
97
+ * @param request Claimed request.
98
+ * @param view Rendered metadata and editor-engine startup failure.
99
+ * @returns Correlated error, otherwise null; prior files and save errors are ignored.
100
+ */
101
+ export function receiptErrorFor(request: EditorRequest, view: ReceiptView): string | null {
102
+ if (!ownsLoad(request, view)) return null
103
+ if (view.engineError) return view.engineError
104
+ return view.load?.status === 'error' ? view.load.error ?? '文件加载失败' : null
105
+ }
106
+
107
+ /**
108
+ * Guard delayed load/render callbacks against a replacement generation.
109
+ * @public @author ddj 2026年09月28号
110
+ * @param load Current committed metadata.
111
+ * @param sessionId Callback owner.
112
+ * @param path Callback target, already normalized as a tab path.
113
+ * @param sequence Callback load sequence.
114
+ * @param currentSequence Live loader sequence.
115
+ * @returns Whether this callback may update the receipt status.
116
+ */
117
+ export function acceptsLoad(load: ReceiptLoad | null, sessionId: string, path: string, sequence: number, currentSequence: number): boolean {
118
+ return Boolean(load && load.sessionId === sessionId && load.path === path
119
+ && load.sequence === sequence && sequence === currentSequence)
120
+ }
package/src/client/rpc.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * 诊断开关运行时化:内存缓存 + localStorage 持久(日志弹窗开关即改即生效,免刷新)。
5
5
  * 作者 ddj 2026-08-20 / 2026-09-17
6
6
  */
7
+ import { appUrl } from '../shared/appUrl.js'
7
8
  import { RPC_PATH } from '../shared/rpc.js'
8
9
  import type { RpcMethod, RpcRequestMap, RpcResult } from '../shared/rpc.js'
9
10
  import type { LoggerLevel } from '../shared/logger.js'
@@ -35,13 +36,38 @@ export function setDebugEnabled(on: boolean): void {
35
36
  } catch (e) { /* 无 localStorage 环境:仅内存生效 */ }
36
37
  }
37
38
 
38
- /** 同源 RPC 调用(host /edrv/rpc 精确路由)。 */
39
- export function rpc<M extends RpcMethod>(method: M, args: RpcRequestMap[M]): Promise<RpcResult<M>> {
40
- return fetch(RPC_PATH, {
39
+ /**
40
+ * Call the authenticated RPC route using the current application base.
41
+ * @author ddj 2026年09月28号
42
+ * @param method Typed business method.
43
+ * @param args Business arguments.
44
+ * @param signal Optional cancellation signal.
45
+ * @returns A validated business result; transport failures reject with a readable error.
46
+ */
47
+ export async function rpc<M extends RpcMethod>(method: M, args: RpcRequestMap[M], signal?: AbortSignal): Promise<RpcResult<M>> {
48
+ const res = await fetch(appUrl(RPC_PATH), {
41
49
  method: 'POST',
50
+ credentials: 'same-origin',
42
51
  headers: { 'content-type': 'application/json' },
43
52
  body: JSON.stringify({ method, args }),
44
- }).then((res) => res.json()) as Promise<RpcResult<M>>
53
+ signal,
54
+ })
55
+ if (!res.ok) {
56
+ const message = res.status === 401 ? 'DSH 登录已失效,请重新打开已登录的应用页面'
57
+ : res.status === 403 ? 'DSH 拒绝了当前页面的访问'
58
+ : res.status === 413 ? '请求超过支持的文件大小'
59
+ : res.status === 503 ? 'DSH 认证服务尚未就绪'
60
+ : 'DSH 请求失败(HTTP ' + res.status + ')'
61
+ throw new Error(message)
62
+ }
63
+ if (!/^application\/json(?:\s*;|\s*$)/i.test(res.headers.get('content-type') ?? '')) {
64
+ throw new Error('DSH 返回了非 JSON 响应,请检查应用地址和代理配置')
65
+ }
66
+ const value: unknown = await res.json()
67
+ if (!value || typeof value !== 'object' || typeof (value as { ok?: unknown }).ok !== 'boolean') {
68
+ throw new Error('DSH RPC 响应格式不正确')
69
+ }
70
+ return value as RpcResult<M>
45
71
  }
46
72
 
47
73
  /**
@@ -0,0 +1,161 @@
1
+ import type { OfficialBinding, OfficialModifier } from '../shared/keybindings.js'
2
+ import type { ShortcutsServiceLike } from './shortcutsOfficial.js'
3
+
4
+ export interface RecordHooks {
5
+ ready(): void
6
+ save(binding: OfficialBinding | null): void
7
+ cancel(): void
8
+ error(error: unknown): void
9
+ }
10
+ const recorderHost = globalThis as typeof globalThis & { __edrvRecording__?: WeakMap<ShortcutsServiceLike, Promise<void>> }
11
+ const leases = recorderHost.__edrvRecording__ ??= new WeakMap<ShortcutsServiceLike, Promise<void>>()
12
+
13
+ /**
14
+ * Serialize native recorder state, including cleanup after a late ready response.
15
+ * @author ddj 2026年09月28号
16
+ * @param service Native/Web shortcut service.
17
+ * @param active Desired native interception state.
18
+ * @returns Completion of the state change; errors remain visible to the caller.
19
+ */
20
+ function recordState(service: ShortcutsServiceLike, active: boolean): Promise<void> {
21
+ const prior = leases.get(service) ?? Promise.resolve()
22
+ const next = prior.catch(() => {}).then(() => service.recording(active))
23
+ leases.set(service, next)
24
+ return next
25
+ }
26
+
27
+ /**
28
+ * Read physical modifiers without converting Control or Meta into logical primary.
29
+ * @author ddj 2026年09月28号
30
+ * @param event Keyboard event.
31
+ * @returns Modifiers in official canonical order.
32
+ */
33
+ export function eventModifiers(event: KeyboardEvent): OfficialModifier[] {
34
+ const states = { control: event.ctrlKey, alt: event.altKey, shift: event.shiftKey, meta: event.metaKey }
35
+ return (Object.keys(states) as Array<keyof typeof states>).filter((key) => states[key])
36
+ }
37
+
38
+ /** Physical key collection; native readiness and listeners are owned by startRecording. */
39
+ export class KeyCapture {
40
+ private held = new Set<string>()
41
+ private pending: OfficialBinding | null = null
42
+ private blocked = false
43
+ private dead = false
44
+ private composing = false
45
+
46
+ /**
47
+ * Create a physical collector with official platform capabilities.
48
+ * @author ddj 2026年09月28号
49
+ * @param service Official binding validator and profile.
50
+ * @param hooks Completion/error handlers.
51
+ */
52
+ constructor(private service: ShortcutsServiceLike, private hooks: RecordHooks) {}
53
+
54
+ /**
55
+ * Reset a gesture on blur or composition transitions.
56
+ * @author ddj 2026年09月28号
57
+ * @param composing Whether IME composition is currently active.
58
+ */
59
+ reset(composing = false): void {
60
+ this.held.clear(); this.pending = null; this.blocked = false; this.dead = false
61
+ this.composing = composing
62
+ }
63
+
64
+ /**
65
+ * Collect one physical press; commit only when a constituent key is released.
66
+ * @author ddj 2026年09月28号
67
+ * @param event Keyboard press; guarded IME/AltGraph input is left untouched.
68
+ */
69
+ down(event: KeyboardEvent): void {
70
+ if (this.composing || event.isComposing || event.keyCode === 229 || event.getModifierState?.('AltGraph')) {
71
+ this.pending = null; this.held.clear(); return
72
+ }
73
+ const commandDead = this.service.runtime === 'web' && this.service.platform === 'macos'
74
+ && event.code === 'KeyN' && event.metaKey && event.altKey && !event.ctrlKey && !event.shiftKey
75
+ if (event.key === 'Dead' && !commandDead) { this.reset(); this.dead = true; return }
76
+ if (this.dead) { this.dead = false; return }
77
+ event.preventDefault(); event.stopPropagation()
78
+ if (event.repeat) return
79
+ const modifiers = eventModifiers(event)
80
+ if (!modifiers.length && event.key === 'Escape') { this.hooks.cancel(); return }
81
+ if (!modifiers.length && (event.key === 'Backspace' || event.key === 'Delete')) { this.hooks.save(null); return }
82
+ if (/^(Control|Alt|Shift|Meta|AltGraph)(Left|Right)?$/u.test(event.code || event.key)) {
83
+ if (this.pending) this.reset()
84
+ return
85
+ }
86
+ const chords = this.service.runtime === 'desktop' && ['macos', 'windows'].includes(this.service.platform)
87
+ if (chords) this.held.add(event.code)
88
+ if (this.blocked) return
89
+ if (this.held.size > 2) {
90
+ this.pending = null; this.blocked = true; this.hooks.error('最多同时录制两个主键'); return
91
+ }
92
+ const codes = chords ? [...this.held] : [event.code]
93
+ try {
94
+ const probe = this.service.describeBinding({ code: codes[0], modifiers, ...(codes[1] ? { secondCode: codes[1] } : {}) })
95
+ if (!probe.binding || probe.issue) throw new Error(probe.issue ?? 'unsupported-key')
96
+ this.pending = probe.binding
97
+ } catch (error) { this.pending = null; this.hooks.error(error) }
98
+ }
99
+
100
+ /**
101
+ * Commit an accepted physical gesture once at release.
102
+ * @author ddj 2026年09月28号
103
+ * @param event Released key or modifier.
104
+ */
105
+ up(event: KeyboardEvent): void {
106
+ this.held.delete(event.code)
107
+ const binding = this.pending
108
+ const modifier = event.code.replace(/(Left|Right)$/u, '').toLowerCase() as OfficialModifier
109
+ if (!binding || !(binding.code === event.code || binding.secondCode === event.code || binding.modifiers.includes(modifier))) return
110
+ this.pending = null
111
+ event.preventDefault(); event.stopPropagation()
112
+ this.hooks.save(binding)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Protect recording before listening, and restore interception on every exit path.
118
+ * @author ddj 2026年09月28号
119
+ * @param service Official service whose asynchronous native bridge must be ready.
120
+ * @param target Owning window, injectable for lifecycle tests.
121
+ * @param hooks Ready, save, cancel and failure callbacks.
122
+ * @returns Idempotent disposal, safe before readiness and after failure.
123
+ */
124
+ export function startRecording(service: ShortcutsServiceLike, target: EventTarget, hooks: RecordHooks): () => void {
125
+ let disposed = false
126
+ let ready = false
127
+ /** @author ddj 2026年09月28号 @param binding Accepted binding; release interception before saving. */
128
+ const save = (binding: OfficialBinding | null): void => { dispose(); hooks.save(binding) }
129
+ /** @author ddj 2026年09月28号 Cancel capture and restore native interception. */
130
+ const cancel = (): void => { dispose(); hooks.cancel() }
131
+ const capture = new KeyCapture(service, { ...hooks, save, cancel })
132
+ /** @author ddj 2026年09月28号 @param event Physical key press. */
133
+ const down = (event: Event): void => { if (ready) capture.down(event as KeyboardEvent) }
134
+ /** @author ddj 2026年09月28号 @param event Physical key release. */
135
+ const up = (event: Event): void => { if (ready) capture.up(event as KeyboardEvent) }
136
+ /** @author ddj 2026年09月28号 Reset incomplete gesture on focus loss. */
137
+ const blur = (): void => capture.reset()
138
+ /** @author ddj 2026年09月28号 Guard the complete IME composition lifecycle. */
139
+ const compose = (): void => capture.reset(true)
140
+ const listeners: Array<[string, EventListener]> = [
141
+ ['keydown', down], ['keyup', up], ['blur', blur], ['compositionstart', compose], ['compositionend', blur],
142
+ ]
143
+ /** @author ddj 2026年09月28号 Release DOM listeners and serialize native restoration. */
144
+ const dispose = (): void => {
145
+ if (disposed) return
146
+ disposed = true; ready = false
147
+ for (const [name, handler] of listeners) target.removeEventListener(name, handler, { capture: true })
148
+ void recordState(service, false).catch(hooks.error)
149
+ }
150
+ /** @author ddj 2026年09月28号 Activate capture only after native acknowledgment. */
151
+ const activate = (): void => {
152
+ if (disposed) return
153
+ ready = true
154
+ for (const [name, handler] of listeners) target.addEventListener(name, handler, { capture: true })
155
+ hooks.ready()
156
+ }
157
+ /** @author ddj 2026年09月28号 @param error Native activation failure. */
158
+ const fail = (error: unknown): void => { if (!disposed) { dispose(); hooks.error(error) } }
159
+ void recordState(service, true).then(activate, fail)
160
+ return dispose
161
+ }