berry-agent 0.1.0-alpha.1
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 +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { decodeMods, decodePhase, KITTY_PUA_KEYS, LETTER_KEYS, NO_MODS, TILDE_KEYS } from './input-keys.js';
|
|
2
|
+
/** 粘贴态防护帽:超帽强制冲刷(恶意/畸形流不锁死解码器——4 MiB) */
|
|
3
|
+
const PASTE_CAP = 4 * 1024 * 1024;
|
|
4
|
+
/** bracketed paste 终界(包裹尾——6 字节 \x1b[201~) */
|
|
5
|
+
const PASTE_END = '\x1b[201~';
|
|
6
|
+
/**
|
|
7
|
+
* rest 尾部可悬置量(0-5):终界 PASTE_END 的最长真前缀后缀长。pty 写缓冲
|
|
8
|
+
* 分块(典型 4-16KiB)可能把 6 字节终界劈成两半——上 chunk 尾部恰是终界前缀
|
|
9
|
+
* 时不能并入粘贴体(并入后下 chunk 永不匹配完整终界 → 解码器滞留 paste 态,
|
|
10
|
+
* 此后键盘输入全被吞进 pasteBuf 零事件),须悬置到实例字段待下 chunk 拼回再搜。
|
|
11
|
+
*/
|
|
12
|
+
function pasteMarkerPrefixLen(rest) {
|
|
13
|
+
const max = Math.min(PASTE_END.length - 1, rest.length);
|
|
14
|
+
for (let k = max; k > 0; k--) {
|
|
15
|
+
if (rest.endsWith(PASTE_END.slice(0, k)))
|
|
16
|
+
return k;
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
/** CSI 参数缓冲防护帽(超帽毒化吞到终点——畸形流防御) */
|
|
21
|
+
const CSI_CAP = 64;
|
|
22
|
+
/** IME 跟随窗(ms):两发 CSI 0 在窗内构成前缀链 = 流式预编辑(kitty 单发提交不受影响) */
|
|
23
|
+
const IME_FOLLOW_WINDOW_MS = 100;
|
|
24
|
+
/**
|
|
25
|
+
* 状态机输入解码器。feed 进 chunk、take 排空事件;settle 在判定窗到点时由
|
|
26
|
+
* 引擎调用(lone-ESC 裁决);discardPending 换防吞在途。
|
|
27
|
+
*/
|
|
28
|
+
export class InputDecoder {
|
|
29
|
+
/** 事件积压队列(take 排空后换新数组——feed 不回调,避免重入) */
|
|
30
|
+
queue = [];
|
|
31
|
+
/**
|
|
32
|
+
* 解析态七分:ground 地面 / esc 转义挂起 / csi 参数积攒 / ss3 / paste
|
|
33
|
+
* 粘贴体 / paste-drain 粘贴吸收态(粘贴态被换防丢弃或超帽冲刷后残余粘贴体
|
|
34
|
+
* 不得按地面态解码成伪造命令行事件——空框 enter 开应用、`/exit`+CRLF 触
|
|
35
|
+
* 退出,吞到真终界 PASTE_END 再回地面)/ mouse-x10 X10 吞态(bare CSI M
|
|
36
|
+
* 后三字节整吞——坐标字节落可打印区间,地面态重解会伪造 text 事件)。
|
|
37
|
+
*/
|
|
38
|
+
mode = 'ground';
|
|
39
|
+
/** CSI 参数积攒缓冲(含私用标记 ?/> 与参数字节) */
|
|
40
|
+
csiBuf = '';
|
|
41
|
+
/** CSI 超帽毒化旗标:序列已判畸形——吞到终点字节整序丢弃(残段不漏成文本) */
|
|
42
|
+
csiPoisoned = false;
|
|
43
|
+
/** 粘贴体积攒缓冲 */
|
|
44
|
+
pasteBuf = '';
|
|
45
|
+
/**
|
|
46
|
+
* 跨 chunk 终界悬置尾(≤5 字节):上 chunk 末尾可能是终界 \x1b[201~ 的真
|
|
47
|
+
* 前缀——悬置于此,下 chunk 先拼回再搜。生命周期:paste 态未命中路径置位;
|
|
48
|
+
* 终界命中 / 防护帽冲刷 / discardPending 清。
|
|
49
|
+
*/
|
|
50
|
+
pendingPasteTail = '';
|
|
51
|
+
/** 地面态可打印文本游程积攒(连续可打印合并单事件——打字/提交不撕裂) */
|
|
52
|
+
textRun = '';
|
|
53
|
+
/** lone-ESC 挂起时刻(null = 无挂起) */
|
|
54
|
+
escPendingAt = null;
|
|
55
|
+
/** IME 挂起预编辑(组字中间态;null = 无组字) */
|
|
56
|
+
preedit = null;
|
|
57
|
+
/** 上一发 CSI 0 文本事件(跟随窗回溯判据——流式预编辑终端的组字态开session锚) */
|
|
58
|
+
lastCsi0 = null;
|
|
59
|
+
/** kitty 协议已应答旗标(DA1 到达时裁 legacy/kitty) */
|
|
60
|
+
kittyAnswered = false;
|
|
61
|
+
/** 协议落定已上报旗标(事件只发一次) */
|
|
62
|
+
protocolReported = false;
|
|
63
|
+
/** X10 吞态剩余字节数(bare CSI M 后整吞 3 字节;0 = 不在吞态) */
|
|
64
|
+
mouseX10Remain = 0;
|
|
65
|
+
/** X10 首达已上报旗标(onMouseLegacy 只发一次——per-entry 闩) */
|
|
66
|
+
mouseLegacyReported = false;
|
|
67
|
+
now;
|
|
68
|
+
escapeWindowMs;
|
|
69
|
+
onProtocol;
|
|
70
|
+
onMouseLegacy;
|
|
71
|
+
constructor(opts = {}) {
|
|
72
|
+
this.now = opts.now ?? Date.now;
|
|
73
|
+
this.escapeWindowMs = opts.escapeWindowMs ?? 30;
|
|
74
|
+
this.onProtocol = opts.onProtocol;
|
|
75
|
+
this.onMouseLegacy = opts.onMouseLegacy;
|
|
76
|
+
}
|
|
77
|
+
/** 组字中旗标(焦面查询面——true 时预编辑在途) */
|
|
78
|
+
get composing() {
|
|
79
|
+
return this.preedit !== null;
|
|
80
|
+
}
|
|
81
|
+
/** 挂起预编辑全文(无则 null——预编辑渲染面取用) */
|
|
82
|
+
get pendingPreedit() {
|
|
83
|
+
return this.preedit;
|
|
84
|
+
}
|
|
85
|
+
/** lone-ESC 挂起中(引擎据此装判定窗定时器) */
|
|
86
|
+
get hasPendingEscape() {
|
|
87
|
+
return this.escPendingAt !== null;
|
|
88
|
+
}
|
|
89
|
+
/** 喂入一个 stdin chunk(字符串已按 UTF-8 解码完成) */
|
|
90
|
+
feed(chunk) {
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < chunk.length) {
|
|
93
|
+
const cp = chunk.codePointAt(i);
|
|
94
|
+
switch (this.mode) {
|
|
95
|
+
case 'ground': {
|
|
96
|
+
if (cp === 0x1b) {
|
|
97
|
+
this.flushTextRun();
|
|
98
|
+
this.mode = 'esc';
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (cp < 0x20 || cp === 0x7f) {
|
|
103
|
+
this.flushTextRun();
|
|
104
|
+
this.dispatchControl(cp);
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// 可打印:游程积攒(代理对整对入游程)
|
|
109
|
+
this.textRun += String.fromCodePoint(cp);
|
|
110
|
+
i += cp > 0xffff ? 2 : 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
case 'esc': {
|
|
114
|
+
if (cp === 0x5b) {
|
|
115
|
+
// '[' —— CSI 序列起
|
|
116
|
+
this.mode = 'csi';
|
|
117
|
+
this.csiBuf = '';
|
|
118
|
+
i++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (cp === 0x4f) {
|
|
122
|
+
// 'O' —— SS3 序列起
|
|
123
|
+
this.mode = 'ss3';
|
|
124
|
+
i++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (cp === 0x1b) {
|
|
128
|
+
// ESC ESC = alt+Escape(legacy alt 编码两字节形)
|
|
129
|
+
this.escPendingAt = null;
|
|
130
|
+
this.emitKey('escape', { ...NO_MODS, alt: true }, 'press');
|
|
131
|
+
this.mode = 'ground';
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// ESC + 可打印 = legacy alt 编码(如 \x1bx → alt+x);控制码罕见——吞
|
|
136
|
+
this.escPendingAt = null;
|
|
137
|
+
if (cp >= 0x20 && cp !== 0x7f) {
|
|
138
|
+
this.emitKey(String.fromCodePoint(cp), { ...NO_MODS, alt: true }, 'press');
|
|
139
|
+
i += cp > 0xffff ? 2 : 1;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
this.mode = 'ground';
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
case 'csi': {
|
|
148
|
+
if (cp >= 0x40 && cp <= 0x7e) {
|
|
149
|
+
// 终点字节到——整序派发(毒化序列整序丢弃)
|
|
150
|
+
if (!this.csiPoisoned)
|
|
151
|
+
this.dispatchCsi(this.csiBuf, String.fromCodePoint(cp));
|
|
152
|
+
this.csiBuf = '';
|
|
153
|
+
this.csiPoisoned = false;
|
|
154
|
+
// dispatchTilde 200 可能已转粘贴态——不覆写(粘贴体接管后续字节)
|
|
155
|
+
if (this.mode === 'csi')
|
|
156
|
+
this.mode = 'ground';
|
|
157
|
+
this.escPendingAt = null;
|
|
158
|
+
i++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
this.csiBuf += String.fromCodePoint(cp);
|
|
162
|
+
i += cp > 0xffff ? 2 : 1;
|
|
163
|
+
if (this.csiBuf.length > CSI_CAP) {
|
|
164
|
+
// 畸形超帽:毒化标记(继续吞参数到终点字节——不锁死、不误发文本)
|
|
165
|
+
this.csiPoisoned = true;
|
|
166
|
+
this.csiBuf = '';
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
case 'ss3': {
|
|
171
|
+
// SS3 单字母终点(A/B/C/D/H/F/P/Q/R/S)
|
|
172
|
+
const key = LETTER_KEYS[String.fromCodePoint(cp)];
|
|
173
|
+
this.escPendingAt = null;
|
|
174
|
+
if (key)
|
|
175
|
+
this.emitKey(key, { ...NO_MODS }, 'press');
|
|
176
|
+
this.mode = 'ground';
|
|
177
|
+
i++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
case 'mouse-x10': {
|
|
181
|
+
// X10 形后三字节整吞(防御吞):坐标字节 0x20+ 落可打印区间——按
|
|
182
|
+
// 地面态重解会伪造 text/按键;降级(DECRST)后不再有后续 X10 报文,
|
|
183
|
+
// 吞态必终止
|
|
184
|
+
i++;
|
|
185
|
+
this.mouseX10Remain--;
|
|
186
|
+
if (this.mouseX10Remain <= 0)
|
|
187
|
+
this.mode = 'ground';
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
case 'paste': {
|
|
191
|
+
// 粘贴体:找包裹尾 PASTE_END——整段交付,体内容不逐键解析。
|
|
192
|
+
// 跨 chunk 终界拼接:上 chunk 尾部的终界真前缀悬置在
|
|
193
|
+
// pendingPasteTail——先拼回再搜;命中时推进量须还账悬置字节(那
|
|
194
|
+
// tail.length 个字节来自上 chunk,本 chunk 未消费)
|
|
195
|
+
const tail = this.pendingPasteTail;
|
|
196
|
+
this.pendingPasteTail = '';
|
|
197
|
+
const rest = tail + chunk.slice(i);
|
|
198
|
+
const end = rest.indexOf(PASTE_END);
|
|
199
|
+
if (end < 0) {
|
|
200
|
+
// 未命中:头部(确定不可能参与终界)并入体积攒;尾部真前缀悬置
|
|
201
|
+
const hold = pasteMarkerPrefixLen(rest);
|
|
202
|
+
this.pasteBuf += rest.slice(0, rest.length - hold);
|
|
203
|
+
this.pendingPasteTail = rest.slice(rest.length - hold);
|
|
204
|
+
i = chunk.length; // 本 chunk 全量消化(并入或悬置——无剩可解)
|
|
205
|
+
this.enforcePasteCap();
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.pasteBuf += rest.slice(0, end);
|
|
209
|
+
const text = this.pasteBuf;
|
|
210
|
+
this.pasteBuf = '';
|
|
211
|
+
this.mode = 'ground';
|
|
212
|
+
this.escPendingAt = null;
|
|
213
|
+
// 终界长 6 > 悬置上限 5 ⇒ 推进量恒 ≥1(无死循环);且 end+6 ≤
|
|
214
|
+
// rest.length 保证不越过本 chunk 末尾
|
|
215
|
+
i += end + PASTE_END.length - tail.length;
|
|
216
|
+
if (text.length > 0)
|
|
217
|
+
this.queue.push({ kind: 'paste', text });
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
case 'paste-drain': {
|
|
222
|
+
// 粘贴吸收态:粘贴体已被换防丢弃/超帽冲刷——残余粘贴体字节全吞(含
|
|
223
|
+
// CRLF/命令形文本,防伪造命令行事件),直到真终界 PASTE_END 到达回
|
|
224
|
+
// 地面。终端协议保证 200~ 后必有 201~——吸收态必终止。跨 chunk 分裂
|
|
225
|
+
// 终界复用 paste 态同款悬置尾(真前缀悬置拼回,不吞真终界字节)
|
|
226
|
+
const tail = this.pendingPasteTail;
|
|
227
|
+
this.pendingPasteTail = '';
|
|
228
|
+
const rest = tail + chunk.slice(i);
|
|
229
|
+
const end = rest.indexOf(PASTE_END);
|
|
230
|
+
if (end < 0) {
|
|
231
|
+
// 未命中:全吞;尾部真前缀悬置(下 chunk 拼回再搜)
|
|
232
|
+
const hold = pasteMarkerPrefixLen(rest);
|
|
233
|
+
this.pendingPasteTail = rest.slice(rest.length - hold);
|
|
234
|
+
i = chunk.length;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// 真终界命中:回地面,续解其后字节(推进量还账悬置字节——同 paste 态)
|
|
238
|
+
this.mode = 'ground';
|
|
239
|
+
this.escPendingAt = null;
|
|
240
|
+
i += end + PASTE_END.length - tail.length;
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// chunk 尽:地面态文本游程冲刷(每 chunk 一个 text 事件——跨 chunk 不滞留;
|
|
247
|
+
// esc/csi/paste 态下 textRun 必为空,冲刷为无操作)
|
|
248
|
+
this.flushTextRun();
|
|
249
|
+
// esc 态挂起无续 → 记 lone-ESC 判定窗起点(首帧记时)
|
|
250
|
+
if (this.mode === 'esc' && this.escPendingAt === null) {
|
|
251
|
+
this.escPendingAt = this.now();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** 判定窗裁决(引擎在窗到点调用):仍挂起 → 判 Esc 键 */
|
|
255
|
+
settle() {
|
|
256
|
+
if (this.mode === 'esc' && this.escPendingAt !== null) {
|
|
257
|
+
if (this.now() - this.escPendingAt >= this.escapeWindowMs) {
|
|
258
|
+
this.escPendingAt = null;
|
|
259
|
+
this.mode = 'ground';
|
|
260
|
+
this.emitKey('escape', { ...NO_MODS }, 'press');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* 换防吞在途:丢弃解析中途的转义/粘贴/预编辑/文本游程——换防瞬间 stdin
|
|
266
|
+
* 在途序列一窗全丢,不漏进新栈。
|
|
267
|
+
*
|
|
268
|
+
* 粘贴态例外:粘贴体半截被弃后残余字节若按地面态重解会伪造命令行事件——
|
|
269
|
+
* 粘贴态/吸收态换防改入吸收态(paste-drain),吞残余到真终界 PASTE_END 再
|
|
270
|
+
* 回地面;CSI/ESC/X10 半序列无续作义务,仍回地面。
|
|
271
|
+
*/
|
|
272
|
+
discardPending() {
|
|
273
|
+
this.mode = this.mode === 'paste' || this.mode === 'paste-drain' ? 'paste-drain' : 'ground';
|
|
274
|
+
this.csiBuf = '';
|
|
275
|
+
this.csiPoisoned = false;
|
|
276
|
+
this.pasteBuf = '';
|
|
277
|
+
this.pendingPasteTail = '';
|
|
278
|
+
this.textRun = '';
|
|
279
|
+
this.escPendingAt = null;
|
|
280
|
+
this.preedit = null;
|
|
281
|
+
this.mouseX10Remain = 0;
|
|
282
|
+
}
|
|
283
|
+
/** 排空事件队列(引擎 emit 的取货口) */
|
|
284
|
+
take() {
|
|
285
|
+
const out = this.queue;
|
|
286
|
+
this.queue = [];
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
/** 游程冲刷:地面态连续可打印合并成一个 text 事件(提交/打字不撕裂) */
|
|
290
|
+
flushTextRun() {
|
|
291
|
+
if (this.textRun.length > 0) {
|
|
292
|
+
this.queue.push({ kind: 'text', text: this.textRun });
|
|
293
|
+
this.textRun = '';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** 按键派发(预编辑挂起时先冲刷为提交——流式组字安全网;phase 必填) */
|
|
297
|
+
emitKey(key, mods, phase) {
|
|
298
|
+
this.flushImeOnInterrupt();
|
|
299
|
+
const ev = { kind: 'key', key, ...mods, phase };
|
|
300
|
+
this.queue.push(ev);
|
|
301
|
+
}
|
|
302
|
+
/** 预编辑被非组字事件打断 → 冲刷为提交(终端漏发显式提交时的兜底) */
|
|
303
|
+
flushImeOnInterrupt() {
|
|
304
|
+
if (this.preedit !== null) {
|
|
305
|
+
const text = this.preedit;
|
|
306
|
+
this.preedit = null;
|
|
307
|
+
const ev = { kind: 'ime', text, committed: true };
|
|
308
|
+
this.queue.push(ev);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** C0 控制码 → 键事件(legacy ctrl 映射表:a-z=0x01-0x1a 等) */
|
|
312
|
+
dispatchControl(cp) {
|
|
313
|
+
if (cp === 0x0d || cp === 0x0a) {
|
|
314
|
+
this.emitKey('enter', { ...NO_MODS }, 'press');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (cp === 0x09) {
|
|
318
|
+
this.emitKey('tab', { ...NO_MODS }, 'press');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (cp === 0x7f) {
|
|
322
|
+
this.emitKey('backspace', { ...NO_MODS }, 'press');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (cp === 0x00) {
|
|
326
|
+
this.emitKey('space', { ...NO_MODS, ctrl: true }, 'press');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (cp >= 0x01 && cp <= 0x1a) {
|
|
330
|
+
// ctrl+字母:0x01=a … 0x1a=z(0x08=h/0x09=i/0x0d=m 由上优先拦走)
|
|
331
|
+
this.emitKey(String.fromCharCode(cp + 96), { ...NO_MODS, ctrl: true }, 'press');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (cp >= 0x1c && cp <= 0x1f) {
|
|
335
|
+
// ctrl+标点(kitty 表:\ ] ^ _)
|
|
336
|
+
this.emitKey(String.fromCharCode(cp + 64), { ...NO_MODS, ctrl: true }, 'press');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// 其余 C0(0x1b 已在状态机先行):吞
|
|
340
|
+
}
|
|
341
|
+
/** CSI 整序派发(params 含私用标记;final 为终点字节) */
|
|
342
|
+
dispatchCsi(params, final) {
|
|
343
|
+
// 私用标记 '?' 开头 = 终端应答(探测/DA1)——协议面处理,绝不误当按键
|
|
344
|
+
if (params.startsWith('?')) {
|
|
345
|
+
if (final === 'u') {
|
|
346
|
+
this.kittyAnswered = true;
|
|
347
|
+
if (!this.protocolReported) {
|
|
348
|
+
this.protocolReported = true;
|
|
349
|
+
this.onProtocol?.('kitty');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (final === 'c') {
|
|
353
|
+
// DA1 应答:kitty 应答从未到 → legacy 落定
|
|
354
|
+
if (!this.protocolReported) {
|
|
355
|
+
this.protocolReported = true;
|
|
356
|
+
this.onProtocol?.(this.kittyAnswered ? 'kitty' : 'legacy');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return; // 其余私用应答(CPR 等)吞
|
|
360
|
+
}
|
|
361
|
+
if (params.startsWith('<')) {
|
|
362
|
+
// SGR 1006 鼠标报文(私用标记 '<' 引导)——协议面处理,绝不误当按键
|
|
363
|
+
this.decodeSgrMouse(params.slice(1), final);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (params === '' && final === 'M') {
|
|
367
|
+
// bare CSI M = X10 形起手(LETTER_KEYS 无 M 键——此形不与功能键冲突):
|
|
368
|
+
// 开了 1006 的会话里 X10 形到达 ⟺ 终端无 SGR 能力——首达上报降级信号
|
|
369
|
+
// (一次性),后三字节转吞态整吞
|
|
370
|
+
if (!this.mouseLegacyReported) {
|
|
371
|
+
this.mouseLegacyReported = true;
|
|
372
|
+
this.onMouseLegacy?.();
|
|
373
|
+
}
|
|
374
|
+
this.mode = 'mouse-x10';
|
|
375
|
+
this.mouseX10Remain = 3;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (final === 'u') {
|
|
379
|
+
this.dispatchKittyKey(params);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (final === '~') {
|
|
383
|
+
this.dispatchTilde(params);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (final === 'Z') {
|
|
387
|
+
this.emitKey('tab', { ...NO_MODS, shift: true }, 'press');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (final === 'R') {
|
|
391
|
+
return; // 光标位置报告应答形状——吞(引擎从不请求,防串扰误键)
|
|
392
|
+
}
|
|
393
|
+
if (final === 'I' || final === 'O') {
|
|
394
|
+
return; // 焦点进出通知——吞(v1 不消费)
|
|
395
|
+
}
|
|
396
|
+
const letterKey = LETTER_KEYS[final];
|
|
397
|
+
if (letterKey) {
|
|
398
|
+
// CSI A/B/C/D/H/F/P-S(`CSI 1;mods C` 修饰形同表)
|
|
399
|
+
const parts = params.split(';');
|
|
400
|
+
this.emitKey(letterKey, decodeMods(parts[1]), 'press');
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// 未知序列:吞(fail-silent——协议噪音吸收,不出垃圾文本)
|
|
404
|
+
}
|
|
405
|
+
/** CSI number[;mods] ~ 功能键派发(200/201 粘贴包裹由 paste 态接管) */
|
|
406
|
+
dispatchTilde(params) {
|
|
407
|
+
const parts = params.split(';');
|
|
408
|
+
if (parts[0] === '200') {
|
|
409
|
+
// bracketed paste 开始:转粘贴体积攒态
|
|
410
|
+
this.mode = 'paste';
|
|
411
|
+
this.pasteBuf = '';
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (parts[0] === '201') {
|
|
415
|
+
return; // 裸包裹尾(无开始)——吞
|
|
416
|
+
}
|
|
417
|
+
const key = TILDE_KEYS[Number(parts[0])];
|
|
418
|
+
if (key)
|
|
419
|
+
this.emitKey(key, decodeMods(parts[1]), 'press');
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* SGR 1006 鼠标报文派发:`CSI < Cb;Cx;Cy M/m`(M = 按压/按住拖动、m = 释放)。
|
|
423
|
+
*
|
|
424
|
+
* - Cb 位域:低两位 = 按钮号(0 左/1 中/2 右;3 无按钮);4=shift / 8=alt /
|
|
425
|
+
* 16=ctrl;32=按住 motion;64 帽位 = 轮系(+0 上/+1 下;+2/+3 = 66/67
|
|
426
|
+
* 水平滚轮左右);128 位 = 扩展按钮 8-11(线值 128-131)。
|
|
427
|
+
* - v1 语义面收窄五钮(left/middle/right/wheel-up/wheel-down):66/67 与
|
|
428
|
+
* 128-131 线值整序吞不产事件;线值 8-11 是 alt 系修饰组合(8=alt+左键
|
|
429
|
+
* 等),位域拆解后照常还原,绝不入吞清单。
|
|
430
|
+
* - 坐标 1 基 → 0 基(types 契约单源 0 基);wheel 无 release(终端不报,
|
|
431
|
+
* 滚轮以 press 一相到达);SGR 位域无 meta 位,meta 恒 false。
|
|
432
|
+
* - 非法参数(NaN/坐标越 0 基界)fail-closed 整序吞。
|
|
433
|
+
*/
|
|
434
|
+
decodeSgrMouse(params, final) {
|
|
435
|
+
if (final !== 'M' && final !== 'm')
|
|
436
|
+
return; // SGR 鼠标只有 M/m 终点——余吞
|
|
437
|
+
const fields = params.split(';');
|
|
438
|
+
const cb = Number(fields[0]);
|
|
439
|
+
const cx = Number(fields[1]);
|
|
440
|
+
const cy = Number(fields[2]);
|
|
441
|
+
if (!Number.isInteger(cb) || !Number.isInteger(cx) || !Number.isInteger(cy))
|
|
442
|
+
return;
|
|
443
|
+
const col = cx - 1;
|
|
444
|
+
const row = cy - 1;
|
|
445
|
+
if (col < 0 || row < 0)
|
|
446
|
+
return;
|
|
447
|
+
const low = cb & 3;
|
|
448
|
+
let button;
|
|
449
|
+
if (cb & 64) {
|
|
450
|
+
if (low === 0)
|
|
451
|
+
button = 'wheel-up';
|
|
452
|
+
else if (low === 1)
|
|
453
|
+
button = 'wheel-down';
|
|
454
|
+
else
|
|
455
|
+
return; // 66/67 水平滚轮——v1 收窄面外,整序吞
|
|
456
|
+
}
|
|
457
|
+
else if (cb & 128) {
|
|
458
|
+
return; // 128-131 扩展按钮 8-11——v1 收窄面外,整序吞
|
|
459
|
+
}
|
|
460
|
+
else if (low === 0)
|
|
461
|
+
button = 'left';
|
|
462
|
+
else if (low === 1)
|
|
463
|
+
button = 'middle';
|
|
464
|
+
else if (low === 2)
|
|
465
|
+
button = 'right';
|
|
466
|
+
else
|
|
467
|
+
return; // 低两位 3 = 无按钮(X10 遗迹/1003 any-motion 形)——不可归因,吞
|
|
468
|
+
const phase = final === 'm' ? 'release' : cb & 32 ? 'motion' : 'press';
|
|
469
|
+
this.queue.push({
|
|
470
|
+
kind: 'mouse',
|
|
471
|
+
phase,
|
|
472
|
+
button,
|
|
473
|
+
col,
|
|
474
|
+
row,
|
|
475
|
+
ctrl: (cb & 16) !== 0,
|
|
476
|
+
alt: (cb & 8) !== 0,
|
|
477
|
+
shift: (cb & 4) !== 0,
|
|
478
|
+
meta: false, // SGR 位域无 meta 位——模型面恒 false(types 契约注记)
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* kitty CSI u 全形派发:
|
|
483
|
+
* `CSI key[:交替键] ; 修饰[:事件型] ; 文本码点 u`
|
|
484
|
+
*/
|
|
485
|
+
dispatchKittyKey(params) {
|
|
486
|
+
const fields = params.split(';');
|
|
487
|
+
const keyParts = (fields[0] ?? '').split(':');
|
|
488
|
+
const keyCode = Number(keyParts[0]);
|
|
489
|
+
if (!Number.isFinite(keyCode))
|
|
490
|
+
return;
|
|
491
|
+
// 修饰子域:值 = 1+位域;冒号后缀 = 事件型(1 press 缺省/2 repeat/3 release)
|
|
492
|
+
const modParts = (fields[1] ?? '').split(':');
|
|
493
|
+
const mods = decodeMods(modParts[0]);
|
|
494
|
+
const phase = decodePhase(modParts.length > 1 ? Number(modParts[1]) : 1);
|
|
495
|
+
// 文本子域:冒号分隔码点列表。码点界检(fail-closed):String.fromCodePoint
|
|
496
|
+
// 对越界/负值/NaN 抛 RangeError——未捕获即杀整进程;Number(' ')===0 静默产
|
|
497
|
+
// NUL。kitty 文本子域结构上只该是 1..0x10FFFF 整数码点,非法子域丢弃
|
|
498
|
+
// (全弃后 keyCode 0 无文本 → 整事件吞——fail-closed,键名码点界检同尺)
|
|
499
|
+
const text = (fields[2] ?? '')
|
|
500
|
+
.split(':')
|
|
501
|
+
.filter((s) => s.length > 0)
|
|
502
|
+
.map((s) => Number(s))
|
|
503
|
+
.filter((n) => Number.isInteger(n) && n >= 1 && n <= 0x10ffff)
|
|
504
|
+
.map((n) => String.fromCodePoint(n))
|
|
505
|
+
.join('');
|
|
506
|
+
// IME/文本优先裁决:key 0 纯文本事件(终端拿不到键信息 = OS/IME 组装)
|
|
507
|
+
if (keyCode === 0 && text.length > 0) {
|
|
508
|
+
this.dispatchImeText(text);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const keyName = this.kittyKeyName(keyCode);
|
|
512
|
+
if (keyName === undefined) {
|
|
513
|
+
// 未知键号:带文本则按提交交付,否则吞
|
|
514
|
+
if (text.length > 0)
|
|
515
|
+
this.dispatchImeText(text);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (text.length > 0) {
|
|
519
|
+
// 文本字段在场:与键符一致(大小写不敏感)= 普通打字 → text 事件;
|
|
520
|
+
// 不一致 = 死键/组合变换文本 → IME 提交路径
|
|
521
|
+
const plain = text.length === 1 && text.toLowerCase() === keyName.toLowerCase();
|
|
522
|
+
if (plain) {
|
|
523
|
+
this.flushImeOnInterrupt();
|
|
524
|
+
this.queue.push({ kind: 'text', text });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
this.dispatchImeText(text);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
this.emitKey(keyName, mods, phase);
|
|
531
|
+
}
|
|
532
|
+
/** kitty 键号 → 规范键名(功能键表 + 可打印码点直映) */
|
|
533
|
+
kittyKeyName(keyCode) {
|
|
534
|
+
if (keyCode === 27)
|
|
535
|
+
return 'escape';
|
|
536
|
+
if (keyCode === 13)
|
|
537
|
+
return 'enter';
|
|
538
|
+
if (keyCode === 9)
|
|
539
|
+
return 'tab';
|
|
540
|
+
if (keyCode === 127)
|
|
541
|
+
return 'backspace';
|
|
542
|
+
const pua = KITTY_PUA_KEYS[keyCode];
|
|
543
|
+
if (pua)
|
|
544
|
+
return pua;
|
|
545
|
+
if (keyCode >= 0x20 && keyCode <= 0x10ffff) {
|
|
546
|
+
const ch = String.fromCodePoint(keyCode);
|
|
547
|
+
if (ch >= ' ')
|
|
548
|
+
return ch; // 可打印(无 shift 修饰原形——kitty 恒小写规范)
|
|
549
|
+
}
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* IME 文本事件裁决(组字态机):
|
|
554
|
+
* - 组字 session 在途:新文本严格扩展挂起预编辑 → 组字中(不落正文,仅供
|
|
555
|
+
* 预编辑渲染);不扩展 → 冲刷为提交。
|
|
556
|
+
* - 无 session:主流终端 CSI 0 只送单发提交 → **立即交付零延迟**。跟随窗内
|
|
557
|
+
* 再来一发且与前发构成前缀链 → 判流式预编辑终端:回溯开 session(前发已
|
|
558
|
+
* 交付不可撤——流式首块漏判是本设计的已知边界,注释在案),本发转组字中。
|
|
559
|
+
*/
|
|
560
|
+
dispatchImeText(text) {
|
|
561
|
+
if (this.preedit !== null && text.length > this.preedit.length && text.startsWith(this.preedit)) {
|
|
562
|
+
this.preedit = text;
|
|
563
|
+
this.queue.push({ kind: 'ime', text, committed: false });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// 无 session:跟随窗内前缀链 → 流式预编辑终端,回溯开 session
|
|
567
|
+
const last = this.lastCsi0;
|
|
568
|
+
if (this.preedit === null &&
|
|
569
|
+
last !== null &&
|
|
570
|
+
this.now() - last.at <= IME_FOLLOW_WINDOW_MS &&
|
|
571
|
+
(text.startsWith(last.text) || last.text.startsWith(text)) &&
|
|
572
|
+
text !== last.text) {
|
|
573
|
+
this.preedit = text;
|
|
574
|
+
this.queue.push({ kind: 'ime', text, committed: false });
|
|
575
|
+
this.lastCsi0 = { text, at: this.now() };
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
this.preedit = null;
|
|
579
|
+
this.lastCsi0 = { text, at: this.now() };
|
|
580
|
+
this.queue.push({ kind: 'ime', text, committed: true });
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* 粘贴体防护帽:超帽强制冲刷并转吸收态(畸形流不锁死键盘)。冲刷后入吸收
|
|
584
|
+
* 态吞残余到真终界 PASTE_END——残余粘贴体按地面态重解会伪造命令行事件。
|
|
585
|
+
*/
|
|
586
|
+
enforcePasteCap() {
|
|
587
|
+
if (this.pasteBuf.length > PASTE_CAP) {
|
|
588
|
+
const text = this.pasteBuf;
|
|
589
|
+
this.pasteBuf = '';
|
|
590
|
+
this.pendingPasteTail = ''; // 悬置尾同弃(已离粘贴态——半截终界无意义)
|
|
591
|
+
this.mode = 'paste-drain';
|
|
592
|
+
if (text.length > 0)
|
|
593
|
+
this.queue.push({ kind: 'paste', text });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export class MemoryTerminalIO {
|
|
2
|
+
/** 累积写出全文(多帧拼接——帧序分析用 frames) */
|
|
3
|
+
bytes = '';
|
|
4
|
+
/** 逐帧记录(每次 write 一项——收帧语义断言用) */
|
|
5
|
+
frames = [];
|
|
6
|
+
/** 几何(测试可设——resize 场景直接改字段后 emitResize) */
|
|
7
|
+
columns;
|
|
8
|
+
rows;
|
|
9
|
+
/** setRawMode 调用史(true = 开 / false = 关,序即调用序) */
|
|
10
|
+
rawModeHistory = [];
|
|
11
|
+
/** 当前 raw 真值(isRaw 查询面——末次 setRawMode 结果) */
|
|
12
|
+
raw = false;
|
|
13
|
+
/** pause / resume 调用计数(挂起交出面断言用) */
|
|
14
|
+
pauseCount = 0;
|
|
15
|
+
resumeCount = 0;
|
|
16
|
+
inputListeners = new Set();
|
|
17
|
+
resizeListeners = new Set();
|
|
18
|
+
constructor(columns = 80, rows = 24) {
|
|
19
|
+
this.columns = columns;
|
|
20
|
+
this.rows = rows;
|
|
21
|
+
}
|
|
22
|
+
/** 写出收帧(帧记录 + 全文累积) */
|
|
23
|
+
write(data) {
|
|
24
|
+
this.frames.push(data);
|
|
25
|
+
this.bytes += data;
|
|
26
|
+
}
|
|
27
|
+
size() {
|
|
28
|
+
return { columns: this.columns, rows: this.rows };
|
|
29
|
+
}
|
|
30
|
+
setRawMode(enable) {
|
|
31
|
+
this.rawModeHistory.push(enable);
|
|
32
|
+
this.raw = enable;
|
|
33
|
+
}
|
|
34
|
+
isRaw() {
|
|
35
|
+
return this.raw;
|
|
36
|
+
}
|
|
37
|
+
pause() {
|
|
38
|
+
this.pauseCount++;
|
|
39
|
+
}
|
|
40
|
+
resume() {
|
|
41
|
+
this.resumeCount++;
|
|
42
|
+
}
|
|
43
|
+
onInput(listener) {
|
|
44
|
+
this.inputListeners.add(listener);
|
|
45
|
+
return () => this.inputListeners.delete(listener);
|
|
46
|
+
}
|
|
47
|
+
onResize(listener) {
|
|
48
|
+
this.resizeListeners.add(listener);
|
|
49
|
+
return () => this.resizeListeners.delete(listener);
|
|
50
|
+
}
|
|
51
|
+
/** 测试注入:驱动全部输入监听器(模拟终端字节流到达——同步派发) */
|
|
52
|
+
emitInput(data) {
|
|
53
|
+
// 快照后派发:派发中增删监听器不外溢到本批(同步退出/复起场景——副屏
|
|
54
|
+
// Engine 在派发中退订并复起主屏监听,新订阅者不吃同批字节;与真终端流
|
|
55
|
+
// 「已派发块不重投」语义一致——批 10f-4 回看器 q 退出路回归锁在
|
|
56
|
+
// tui-backend.test.ts /history 副屏装配组)
|
|
57
|
+
for (const listener of [...this.inputListeners])
|
|
58
|
+
listener(data);
|
|
59
|
+
}
|
|
60
|
+
/** 测试注入:驱动全部 resize 监听器(几何已由测试侧先行改定) */
|
|
61
|
+
emitResize() {
|
|
62
|
+
// 快照后派发(与 emitInput 同律——派发中增删不外溢)
|
|
63
|
+
for (const listener of [...this.resizeListeners])
|
|
64
|
+
listener();
|
|
65
|
+
}
|
|
66
|
+
/** 清输出账(长测试分段断言用——监听器几何保留) */
|
|
67
|
+
reset() {
|
|
68
|
+
this.bytes = '';
|
|
69
|
+
this.frames.length = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|