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,403 @@
|
|
|
1
|
+
import { graphemeWidth, splitGraphemes } from '../../engine/index.js';
|
|
2
|
+
import { ScrollView } from '../scroll/scroll-view.js';
|
|
3
|
+
import { Editor } from '../editor/editor.js';
|
|
4
|
+
import { prefixDisplayWidth } from '../editor/visual-lines.js';
|
|
5
|
+
import { LiveTranscript, renderBlockStyledLines, shortIdOf } from '../backend/transcript.js';
|
|
6
|
+
import { sessionColor } from '../theme.js';
|
|
7
|
+
/** 当前匹配高亮样式(整段反色——视口内最强存在感;选区高亮同载体叠加) */
|
|
8
|
+
const MATCH_STYLE = Object.freeze({ inverse: true });
|
|
9
|
+
/** 提示行样式(dim——存在感弱于正文) */
|
|
10
|
+
const HINT_STYLE = Object.freeze({ dim: true });
|
|
11
|
+
/** 常态底行键面提示 */
|
|
12
|
+
const HINT_TEXT = 'q/esc 返回 · ctrl+shift+f 搜索 · ↑↓/pgup/pgdn/home/end 滚动 · 拖选复制';
|
|
13
|
+
/** 选区帽(64 KiB——07 件 8 细则码面缺省参数;计量面 = 选中明文 UTF-8 字节数,与 xterm 100,000 解码后上限同基准) */
|
|
14
|
+
const SELECTION_CAP_BYTES = 64 * 1024;
|
|
15
|
+
/** 超帽底行提示(spec 定文——常显至选区清除) */
|
|
16
|
+
const SELECTION_CAP_NOTICE = '选区过大未复制';
|
|
17
|
+
/** key 事件窄化(其他事件形归各分路——text/ime/paste) */
|
|
18
|
+
function asKey(event) {
|
|
19
|
+
return event.kind === 'key' ? event : null;
|
|
20
|
+
}
|
|
21
|
+
/** 无修饰命名键判(让位判据与退出键消费的判据面) */
|
|
22
|
+
function isPlainKey(e, key) {
|
|
23
|
+
return !e.ctrl && !e.alt && !e.shift && !e.meta && e.key === key;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 回看器内容件:ScrollView 子类(滚动/折叠/偏移算术继承)+ OverlayContent
|
|
27
|
+
* (副屏 root——render 直收全屏 region,量高不经布局路)。
|
|
28
|
+
*/
|
|
29
|
+
export class HistoryViewer extends ScrollView {
|
|
30
|
+
sessionId;
|
|
31
|
+
shortId;
|
|
32
|
+
/** 带样式行集(构造后静态——快照档 v1:回看期新事件不进副屏) */
|
|
33
|
+
styledLines;
|
|
34
|
+
onExit;
|
|
35
|
+
onInterrupt;
|
|
36
|
+
onQuit;
|
|
37
|
+
onCopy;
|
|
38
|
+
/* ---- 搜索态(开/跳/关三动作状态机) ---- */
|
|
39
|
+
searchOpen = false;
|
|
40
|
+
searchEditor;
|
|
41
|
+
matches = [];
|
|
42
|
+
matchIndex = -1;
|
|
43
|
+
/** 退出闭锁(同批多事件只退一次——q 与 Esc 竞发的防御位) */
|
|
44
|
+
exited = false;
|
|
45
|
+
/* ---- 选区态(press 锚定 → motion 扩展 → release 复制——线性选区状态机) ---- */
|
|
46
|
+
selAnchor = null;
|
|
47
|
+
selFocus = null;
|
|
48
|
+
/** 底行提示文案(超帽拒复制——常显至选区清除;null = 常态键面提示) */
|
|
49
|
+
selectionNotice = null;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
super(); // 无 maxHeight——副屏 root 直收 region 全高
|
|
52
|
+
this.sessionId = options.sessionId;
|
|
53
|
+
this.shortId = shortIdOf(options.sessionId);
|
|
54
|
+
this.onExit = options.onExit;
|
|
55
|
+
this.onInterrupt = options.onInterrupt;
|
|
56
|
+
this.onQuit = options.onQuit;
|
|
57
|
+
this.onCopy = options.onCopy;
|
|
58
|
+
// 全量档行集:投影 → LiveTranscript(Infinity 帽恒不截)→ 带样式行(管线单源)
|
|
59
|
+
const transcript = new LiveTranscript({ blockCap: Number.POSITIVE_INFINITY });
|
|
60
|
+
transcript.loadProjection(options.messages);
|
|
61
|
+
const styled = [];
|
|
62
|
+
for (const block of transcript.snapshot) {
|
|
63
|
+
styled.push(...renderBlockStyledLines(block, options.columns));
|
|
64
|
+
}
|
|
65
|
+
this.styledLines = styled;
|
|
66
|
+
super.setLines(styled.map((line) => line.plain)); // 开屏贴尾(follow 初始 true)
|
|
67
|
+
this.searchEditor = new Editor({
|
|
68
|
+
maxVisibleLines: 1, // 单行档——搜索框
|
|
69
|
+
onChange: () => this.recomputeMatches(),
|
|
70
|
+
});
|
|
71
|
+
this.searchEditor.setFocused(false);
|
|
72
|
+
}
|
|
73
|
+
/** 量高:头行 + 视口全量 + 底铬(副屏 root 不经布局路——契约的诚实实现) */
|
|
74
|
+
measure(width) {
|
|
75
|
+
return 1 + super.measure(width) + (this.searchOpen ? this.searchEditor.measure(width) : 1);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 落位(副屏全屏 region 自底盘向上三段):头行(档名 + 会话短 id 会话区分色
|
|
79
|
+
* + 搜索计数)→ 滚动视口(super.render——折叠/偏移/滚动条恒归 ScrollView)→
|
|
80
|
+
* 底铬(搜索在场 = 单行 Editor;否则键面提示行)。
|
|
81
|
+
*/
|
|
82
|
+
render(buffer, region) {
|
|
83
|
+
if (region.height < 2)
|
|
84
|
+
return; // 防御位(极小终端——头行 + 视口都不够)
|
|
85
|
+
// 头行
|
|
86
|
+
const headPrefix = '↩ 历史回看 · ';
|
|
87
|
+
buffer.writeText(region.row, region.col, headPrefix);
|
|
88
|
+
// 会话短 id 起列 = 前缀显示宽(CJK 双宽——UTF-16 长度直减不可靠)
|
|
89
|
+
buffer.writeText(region.row, region.col + prefixDisplayWidth(headPrefix, headPrefix.length), this.shortId, {
|
|
90
|
+
fg: sessionColor(this.shortId),
|
|
91
|
+
});
|
|
92
|
+
if (this.searchOpen) {
|
|
93
|
+
// 搜索计数(当前/总数;无匹配如实 0)
|
|
94
|
+
const count = `${this.matchIndex + 1}/${this.matches.length}`;
|
|
95
|
+
buffer.writeText(region.row, region.col + region.width - 1 - count.length, count, HINT_STYLE);
|
|
96
|
+
}
|
|
97
|
+
// 滚动视口(头行与底铬之间)
|
|
98
|
+
const chromeBottom = this.searchOpen ? this.searchEditor.measure(region.width) : 1;
|
|
99
|
+
const viewHeight = region.height - 1 - chromeBottom;
|
|
100
|
+
if (viewHeight > 0) {
|
|
101
|
+
super.render(buffer, { row: region.row + 1, col: region.col, width: region.width, height: viewHeight });
|
|
102
|
+
}
|
|
103
|
+
// 底铬
|
|
104
|
+
if (this.searchOpen) {
|
|
105
|
+
this.searchEditor.setFocused(true);
|
|
106
|
+
this.searchEditor.render(buffer, {
|
|
107
|
+
row: region.row + region.height - chromeBottom,
|
|
108
|
+
col: region.col,
|
|
109
|
+
width: region.width,
|
|
110
|
+
height: chromeBottom,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// 底行:超帽提示优先(常显至选区清除),否则常态键面提示
|
|
115
|
+
buffer.writeText(region.row + region.height - 1, region.col, this.selectionNotice ?? HINT_TEXT, HINT_STYLE);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 事件分发(副屏内容终局消费——未消费键不穿透,模态独占)。
|
|
120
|
+
* 序:Ctrl+C / Ctrl+D 副屏键面补丁 → 搜索开关键 → 搜索在场模态(让位判据:
|
|
121
|
+
* q/Esc 不退出)→ 常态退出键(q/Esc)→ 滚动键(ScrollView)。
|
|
122
|
+
*/
|
|
123
|
+
handleEvent(event) {
|
|
124
|
+
if (event.kind === 'mouse')
|
|
125
|
+
return this.handleMouse(event); // mouse 路(终局消费——模态独占)
|
|
126
|
+
const k = asKey(event);
|
|
127
|
+
if (k !== null && k.phase !== 'release') {
|
|
128
|
+
// Ctrl+C = 打断在飞 run(press/repeat 相动作滤 kitty release——与滚动键/编辑键的相过滤同律)
|
|
129
|
+
if (k.ctrl && !k.alt && !k.shift && !k.meta && k.key === 'c') {
|
|
130
|
+
this.onInterrupt?.(this.sessionId);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
// Ctrl+D = 退出(先收副屏再转退出柄;搜索框有文不退——主屏空框闸同律)
|
|
134
|
+
if (k.ctrl && !k.alt && !k.shift && !k.meta && k.key === 'd' && this.searchEditor.model.isEmpty()) {
|
|
135
|
+
this.exit();
|
|
136
|
+
this.onQuit?.();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
// 搜索开键(条款锁能力不锁键位):kitty 轨 ctrl+shift+f;legacy 轨 0x06
|
|
140
|
+
// 归一 ctrl+f(shift 不可辨)——搜索关态两形同判,开态 ctrl+f 让路编辑器
|
|
141
|
+
if (k.ctrl && !k.alt && !k.meta && k.key === 'f' && (k.shift || !this.searchOpen)) {
|
|
142
|
+
if (!this.searchOpen)
|
|
143
|
+
this.openSearch();
|
|
144
|
+
return true; // 已开态的 ctrl+shift+f = no-op(层内终局)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (this.searchOpen) {
|
|
148
|
+
// 让位判据(条款):搜索框在场时 Esc 先关搜索、q 落框内——退出键不消费
|
|
149
|
+
if (k !== null && k.phase !== 'release' && !k.ctrl && !k.alt && !k.meta) {
|
|
150
|
+
if (isPlainKey(k, 'escape')) {
|
|
151
|
+
this.closeSearch();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (isPlainKey(k, 'enter')) {
|
|
155
|
+
this.jumpMatch(1); // 跳匹配:Enter 下一
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (k.shift && k.key === 'enter') {
|
|
159
|
+
this.jumpMatch(-1); // Shift+Enter 上一
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 其余(文本/IME/粘贴/编辑键)入搜索框;未消费键层内终局(模态)
|
|
164
|
+
this.searchEditor.handleEvent(event);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// 常态退出键(搜索不在场——让位判据的另一面):q(text 路为主——kitty
|
|
168
|
+
// disambiguate 轨纯键打字走 text 事件;key 路防御同判)与 Esc
|
|
169
|
+
if (k !== null && k.phase !== 'release') {
|
|
170
|
+
if (isPlainKey(k, 'escape') || isPlainKey(k, 'q')) {
|
|
171
|
+
this.exit();
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (event.kind === 'text' && event.text === 'q') {
|
|
176
|
+
this.exit();
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
// 滚动键(↑/↓/PgUp/PgDn/Home/End——ScrollView 自持;未消费键终局吞)
|
|
180
|
+
this.handleEventSuper(event);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
/** super.handleEvent 直呼(子类覆写同名面后仍需触达基类滚动路的显式位) */
|
|
184
|
+
handleEventSuper(event) {
|
|
185
|
+
super.handleEvent(event);
|
|
186
|
+
}
|
|
187
|
+
/* ---------------- 鼠标选区(mu-2——07 件 8 细则) ---------------- */
|
|
188
|
+
/**
|
|
189
|
+
* mouse 事件路(副屏内容件终局消费——未消费 mouse 不穿透,模态独占):
|
|
190
|
+
* 滚轮归 ScrollView(搜索在场也照常滚——禁的是拖选);选区 = 左键 press
|
|
191
|
+
* 锚定 → motion 扩展 → release 复制(release 后选区高亮保留,下次 press
|
|
192
|
+
* 清除/重锚)。
|
|
193
|
+
*/
|
|
194
|
+
handleMouse(event) {
|
|
195
|
+
if (event.button === 'wheel-up' || event.button === 'wheel-down') {
|
|
196
|
+
this.handleEventSuper(event); // 滚轮消费路(±3 视觉行——显式滚动路)
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (this.searchOpen)
|
|
200
|
+
return true; // 搜索框在场拖选禁用(输入模态优先)——吞
|
|
201
|
+
if (event.button !== 'left')
|
|
202
|
+
return true; // v1 选区只有左键——中/右零动作吞
|
|
203
|
+
if (event.phase === 'press') {
|
|
204
|
+
// 新选区起手:旧选区与超帽提示随清(「常显至选区清除」的清除位)
|
|
205
|
+
this.selectionNotice = null;
|
|
206
|
+
const hit = this.screenToLogical(event.row, event.col);
|
|
207
|
+
this.selAnchor = hit; // 视口外起手 = null(无锚——motion/release 零动作)
|
|
208
|
+
this.selFocus = hit; // 零宽起手(拖动才扩)
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (this.selAnchor === null)
|
|
212
|
+
return true; // 无锚——motion/release 零动作
|
|
213
|
+
if (event.phase === 'motion') {
|
|
214
|
+
const hit = this.screenToLogical(event.row, event.col);
|
|
215
|
+
if (hit !== null)
|
|
216
|
+
this.selFocus = hit; // 拖出视口(头行/底铬/滚动条列)保焦点不扩
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
// release:空选区零动作;超帽拒复制 + 底行提示;在帽行间拼 LF 经注入柄写出
|
|
220
|
+
const text = this.selectionText();
|
|
221
|
+
if (text.length === 0)
|
|
222
|
+
return true;
|
|
223
|
+
if (Buffer.byteLength(text, 'utf8') > SELECTION_CAP_BYTES) {
|
|
224
|
+
this.selectionNotice = SELECTION_CAP_NOTICE;
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
this.onCopy?.(text);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 屏幕坐标 → 选区定位点(逻辑行 + 行内 UTF-16 下标):视口外命中(头行/
|
|
232
|
+
* 底铬/滚动条列/界外)返 null 零动作;列反查走字素宽累加(与折叠算术同源
|
|
233
|
+
* ——CJK 双宽对齐,命中字素含半格命中归该字素首)。
|
|
234
|
+
*/
|
|
235
|
+
screenToLogical(row, col) {
|
|
236
|
+
const geo = this.viewportGeometry;
|
|
237
|
+
if (row < 1 || row >= 1 + geo.height)
|
|
238
|
+
return null; // 头行 / 底铬(拖选仅在搜索不在场——底铬恒提示行 1 行)
|
|
239
|
+
if (col < 0 || col >= geo.width)
|
|
240
|
+
return null; // 界外防御位
|
|
241
|
+
if (this.hitScrollbar(col))
|
|
242
|
+
return null; // 滚动条列(溢出让列时末列)零动作
|
|
243
|
+
const seg = this.segmentAt(row - 1);
|
|
244
|
+
if (seg === null)
|
|
245
|
+
return null;
|
|
246
|
+
const text = this.styledLines[seg.line]?.plain ?? '';
|
|
247
|
+
let index = seg.startCol;
|
|
248
|
+
let seen = 0;
|
|
249
|
+
for (const g of splitGraphemes(text.slice(seg.startCol, seg.startCol + seg.length))) {
|
|
250
|
+
if (seen + graphemeWidth(g) > col)
|
|
251
|
+
break; // 命中字素(含双宽字素半格)归该字素首
|
|
252
|
+
seen += graphemeWidth(g);
|
|
253
|
+
index += g.length;
|
|
254
|
+
}
|
|
255
|
+
return { line: seg.line, col: index };
|
|
256
|
+
}
|
|
257
|
+
/** 选区规范化(锚/焦点 → 升序两端点;无选区 null) */
|
|
258
|
+
selectionBounds() {
|
|
259
|
+
if (this.selAnchor === null || this.selFocus === null)
|
|
260
|
+
return null;
|
|
261
|
+
const flip = this.selFocus.line < this.selAnchor.line ||
|
|
262
|
+
(this.selFocus.line === this.selAnchor.line && this.selFocus.col < this.selAnchor.col);
|
|
263
|
+
return flip ? [this.selFocus, this.selAnchor] : [this.selAnchor, this.selFocus];
|
|
264
|
+
}
|
|
265
|
+
/** 选区明文(行间拼 LF——release 复制与帽计量的单源;空选区空串) */
|
|
266
|
+
selectionText() {
|
|
267
|
+
const bounds = this.selectionBounds();
|
|
268
|
+
if (bounds === null)
|
|
269
|
+
return '';
|
|
270
|
+
const [from, to] = bounds;
|
|
271
|
+
const parts = [];
|
|
272
|
+
for (let line = from.line; line <= to.line; line++) {
|
|
273
|
+
const plain = this.styledLines[line]?.plain ?? '';
|
|
274
|
+
const start = line === from.line ? from.col : 0;
|
|
275
|
+
const end = line === to.line ? to.col : plain.length;
|
|
276
|
+
parts.push(plain.slice(start, end));
|
|
277
|
+
}
|
|
278
|
+
return parts.join('\n');
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* 本逻辑行的选中区间(端点含头不含尾;end = Infinity 表行尾开放——跨行尾
|
|
282
|
+
* 段;无选区/异行 null)。writeSlice 高亮与 selectionText 共用此规范化。
|
|
283
|
+
*/
|
|
284
|
+
selectionSpanFor(line) {
|
|
285
|
+
const bounds = this.selectionBounds();
|
|
286
|
+
if (bounds === null)
|
|
287
|
+
return null;
|
|
288
|
+
const [from, to] = bounds;
|
|
289
|
+
if (line < from.line || line > to.line)
|
|
290
|
+
return null;
|
|
291
|
+
return {
|
|
292
|
+
start: line === from.line ? from.col : 0,
|
|
293
|
+
end: line === to.line ? to.col : Number.POSITIVE_INFINITY,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/* ---------------- 搜索三动作 ---------------- */
|
|
297
|
+
/** 开:搜索框在场 + 按在框文本重算匹配并跳首匹配 */
|
|
298
|
+
openSearch() {
|
|
299
|
+
this.searchOpen = true;
|
|
300
|
+
this.recomputeMatches();
|
|
301
|
+
}
|
|
302
|
+
/** 关:框退场 + 高亮清(查询文本保留——重开续搜的增量检索延续) */
|
|
303
|
+
closeSearch() {
|
|
304
|
+
this.searchOpen = false;
|
|
305
|
+
this.matches = [];
|
|
306
|
+
this.matchIndex = -1;
|
|
307
|
+
this.searchEditor.setFocused(false);
|
|
308
|
+
}
|
|
309
|
+
/** 匹配重算(查询变更路):全行不区分大小写子串扫描 + 跳首匹配 */
|
|
310
|
+
recomputeMatches() {
|
|
311
|
+
const query = this.searchEditor.getText();
|
|
312
|
+
const found = [];
|
|
313
|
+
if (query !== '') {
|
|
314
|
+
const q = query.toLowerCase();
|
|
315
|
+
for (let i = 0; i < this.styledLines.length; i++) {
|
|
316
|
+
const plain = this.styledLines[i].plain.toLowerCase();
|
|
317
|
+
let at = plain.indexOf(q);
|
|
318
|
+
while (at !== -1) {
|
|
319
|
+
found.push({ line: i, start: at, end: at + q.length });
|
|
320
|
+
at = plain.indexOf(q, at + 1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.matches = found;
|
|
325
|
+
this.matchIndex = found.length > 0 ? 0 : -1;
|
|
326
|
+
if (this.matchIndex >= 0)
|
|
327
|
+
this.scrollToLine(found[0].line, found[0].start);
|
|
328
|
+
}
|
|
329
|
+
/** 跳匹配(±1 循环——尾后回首、首前到尾) */
|
|
330
|
+
jumpMatch(delta) {
|
|
331
|
+
if (this.matches.length === 0)
|
|
332
|
+
return;
|
|
333
|
+
this.matchIndex = (this.matchIndex + delta + this.matches.length) % this.matches.length;
|
|
334
|
+
const m = this.matches[this.matchIndex];
|
|
335
|
+
this.scrollToLine(m.line, m.start);
|
|
336
|
+
}
|
|
337
|
+
/** 退出(闭锁——单次) */
|
|
338
|
+
exit() {
|
|
339
|
+
if (this.exited)
|
|
340
|
+
return;
|
|
341
|
+
this.exited = true;
|
|
342
|
+
this.onExit?.();
|
|
343
|
+
}
|
|
344
|
+
/* ---------------- 带样式切片写出(writeSlice 覆写接缝) ---------------- */
|
|
345
|
+
/**
|
|
346
|
+
* 视口切片带样式写出:样式段(管线单源产物)按切片边界分段落格 + 当前匹配
|
|
347
|
+
* 反色高亮叠加。列位经 prefixDisplayWidth 换算(UTF-16 切片下标 → 显示列
|
|
348
|
+
* ——CJK 双宽对齐与折叠算术同源)。
|
|
349
|
+
*/
|
|
350
|
+
writeSlice(buffer, region, displayRow, seg) {
|
|
351
|
+
const styled = this.styledLines[seg.line];
|
|
352
|
+
if (styled === undefined)
|
|
353
|
+
return;
|
|
354
|
+
const start = seg.startCol;
|
|
355
|
+
const end = seg.startCol + seg.length;
|
|
356
|
+
// 当前匹配与本切片的交叠段(无匹配/异行 = 空区间)
|
|
357
|
+
const match = this.matchIndex >= 0 ? this.matches[this.matchIndex] : undefined;
|
|
358
|
+
const hlStart = match !== undefined && match.line === seg.line ? Math.max(match.start, start) : end;
|
|
359
|
+
const hlEnd = match !== undefined && match.line === seg.line ? Math.min(match.end, end) : start;
|
|
360
|
+
// 选区段(本切片所在逻辑行的选中区间——与当前匹配同 inverse 载体叠加)
|
|
361
|
+
const sel = this.selectionSpanFor(seg.line);
|
|
362
|
+
const selStart = sel !== null ? Math.max(sel.start, start) : end;
|
|
363
|
+
const selEnd = sel !== null ? Math.min(sel.end, end) : start;
|
|
364
|
+
// 分段边界:切片端点 ∪ 样式段端点 ∪ 高亮端点 ∪ 选区端点(升序去重)
|
|
365
|
+
const cuts = new Set([start, end]);
|
|
366
|
+
for (const run of styled.runs) {
|
|
367
|
+
if (run.start > start && run.start < end)
|
|
368
|
+
cuts.add(run.start);
|
|
369
|
+
if (run.end > start && run.end < end)
|
|
370
|
+
cuts.add(run.end);
|
|
371
|
+
}
|
|
372
|
+
if (hlStart > start && hlStart < end)
|
|
373
|
+
cuts.add(hlStart);
|
|
374
|
+
if (hlEnd > start && hlEnd < end)
|
|
375
|
+
cuts.add(hlEnd);
|
|
376
|
+
if (selStart > start && selStart < end)
|
|
377
|
+
cuts.add(selStart);
|
|
378
|
+
if (selEnd > start && selEnd < end)
|
|
379
|
+
cuts.add(selEnd);
|
|
380
|
+
const bounds = [...cuts].sort((a, b) => a - b);
|
|
381
|
+
const baseCols = prefixDisplayWidth(styled.plain, start);
|
|
382
|
+
for (let i = 0; i + 1 < bounds.length; i++) {
|
|
383
|
+
const from = bounds[i];
|
|
384
|
+
const to = bounds[i + 1];
|
|
385
|
+
// 段样式:覆写 from 起点的样式段(段间空隙 = 裸文本)
|
|
386
|
+
let style;
|
|
387
|
+
for (const run of styled.runs) {
|
|
388
|
+
if (run.start <= from && from < run.end) {
|
|
389
|
+
style = run.style;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (from >= hlStart && from < hlEnd) {
|
|
394
|
+
style = style === undefined ? MATCH_STYLE : { ...style, inverse: true };
|
|
395
|
+
}
|
|
396
|
+
if (from >= selStart && from < selEnd) {
|
|
397
|
+
// 选区高亮(与搜索匹配同 inverse 载体——叠加处同 inverse 幂等)
|
|
398
|
+
style = style === undefined ? MATCH_STYLE : { ...style, inverse: true };
|
|
399
|
+
}
|
|
400
|
+
buffer.writeText(region.row + displayRow, region.col + prefixDisplayWidth(styled.plain, from) - baseCols, styled.plain.slice(from, to), style);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Flex 结构判据(isFlex 真值 = Column 吸收余量的信号位) */
|
|
2
|
+
export function isFlexRenderable(r) {
|
|
3
|
+
return r.isFlex === true;
|
|
4
|
+
}
|
|
5
|
+
/** 区域取交(越界返回 null——布局夹取的公共件) */
|
|
6
|
+
function intersect(a, b) {
|
|
7
|
+
const row1 = Math.max(a.row, b.row);
|
|
8
|
+
const col1 = Math.max(a.col, b.col);
|
|
9
|
+
const row2 = Math.min(a.row + a.height, b.row + b.height);
|
|
10
|
+
const col2 = Math.min(a.col + a.width, b.col + b.width);
|
|
11
|
+
if (row2 <= row1 || col2 <= col1)
|
|
12
|
+
return null;
|
|
13
|
+
return { row: row1, col: col1, width: col2 - col1, height: row2 - row1 };
|
|
14
|
+
}
|
|
15
|
+
/** Flex 包装件(isFlex 标记——Column 吸收余量的信号;协商最小占位 1) */
|
|
16
|
+
export class Flex {
|
|
17
|
+
isFlex = true;
|
|
18
|
+
child;
|
|
19
|
+
constructor(props) {
|
|
20
|
+
this.child = props.child;
|
|
21
|
+
}
|
|
22
|
+
measure(_width) {
|
|
23
|
+
return 1; // 协商最小占位(真高度由 Column 分配余量决定)
|
|
24
|
+
}
|
|
25
|
+
render(buffer, region) {
|
|
26
|
+
if (region.height <= 0 || region.width <= 0)
|
|
27
|
+
return;
|
|
28
|
+
this.child.render(buffer, region);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 垂直堆叠组合子:固定子按 measure(width) 定高顺排;Flex 子均分余量
|
|
33
|
+
* (整除余数给最后一个 Flex 子)。子区域宽 = 全宽;底部溢出截断。
|
|
34
|
+
*/
|
|
35
|
+
export class Column {
|
|
36
|
+
children;
|
|
37
|
+
constructor(props) {
|
|
38
|
+
this.children = props.children;
|
|
39
|
+
}
|
|
40
|
+
measure(width) {
|
|
41
|
+
// 协商面:固定子按各自期望;Flex 子按最小占位 1 计
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const c of this.children) {
|
|
44
|
+
total += isFlexRenderable(c) ? 1 : Math.max(1, c.measure(width));
|
|
45
|
+
}
|
|
46
|
+
return Math.max(1, total);
|
|
47
|
+
}
|
|
48
|
+
render(buffer, region) {
|
|
49
|
+
if (region.width <= 0 || region.height <= 0)
|
|
50
|
+
return;
|
|
51
|
+
// 两遍:先算固定高合计与 Flex 子数,再逐子分配落位
|
|
52
|
+
const fixed = [];
|
|
53
|
+
let fixedSum = 0;
|
|
54
|
+
let flexCount = 0;
|
|
55
|
+
for (const c of this.children) {
|
|
56
|
+
if (isFlexRenderable(c)) {
|
|
57
|
+
fixed.push(-1);
|
|
58
|
+
flexCount++;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const h = Math.max(1, c.measure(region.width));
|
|
62
|
+
fixed.push(h);
|
|
63
|
+
fixedSum += h;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const remaining = Math.max(0, region.height - fixedSum);
|
|
67
|
+
const flexH = flexCount > 0 ? Math.max(1, Math.floor(remaining / flexCount)) : 0;
|
|
68
|
+
let flexSeen = 0;
|
|
69
|
+
let row = region.row;
|
|
70
|
+
for (let idx = 0; idx < this.children.length; idx++) {
|
|
71
|
+
if (row >= region.row + region.height)
|
|
72
|
+
break; // 底部溢出截断
|
|
73
|
+
const child = this.children[idx];
|
|
74
|
+
let h;
|
|
75
|
+
if (fixed[idx] < 0) {
|
|
76
|
+
flexSeen++;
|
|
77
|
+
// 末 Flex 子收整除余数(份额 = 余量 - 前 N-1 份),仍受底部截断约束
|
|
78
|
+
const share = flexSeen === flexCount ? remaining - flexH * (flexCount - 1) : flexH;
|
|
79
|
+
h = Math.min(Math.max(0, share), region.row + region.height - row);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
h = Math.min(fixed[idx], region.row + region.height - row);
|
|
83
|
+
}
|
|
84
|
+
if (h > 0) {
|
|
85
|
+
child.render(buffer, { row, col: region.col, width: region.width, height: h });
|
|
86
|
+
}
|
|
87
|
+
row += h;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** 水平分列组合子:按权重分宽(整除余数给末列);高度 = 各子期望最大值 */
|
|
92
|
+
export class Row {
|
|
93
|
+
children;
|
|
94
|
+
weights;
|
|
95
|
+
constructor(props) {
|
|
96
|
+
this.children = props.children;
|
|
97
|
+
this.weights = props.weights ?? props.children.map(() => 1);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 在总宽 width 下各子分得的列宽(含余数吸收——render 与 measure 共用
|
|
101
|
+
* 单源:两段协商「同一分配单源」条款的执法位)。
|
|
102
|
+
*/
|
|
103
|
+
allocate(width) {
|
|
104
|
+
const totalW = this.weights.reduce((a, b) => a + b, 0);
|
|
105
|
+
const out = [];
|
|
106
|
+
let used = 0;
|
|
107
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
108
|
+
// 末列收整除余数(宽 = 总宽 - 前已分),前列按权重整除
|
|
109
|
+
const w = i === this.children.length - 1 ? width - used : Math.floor((width * (this.weights[i] ?? 1)) / totalW);
|
|
110
|
+
out.push(Math.max(0, w));
|
|
111
|
+
used += Math.max(0, w);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
measure(width) {
|
|
116
|
+
const widths = this.allocate(Math.max(1, width));
|
|
117
|
+
let max = 1;
|
|
118
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
119
|
+
max = Math.max(max, this.children[i].measure(Math.max(1, widths[i])));
|
|
120
|
+
}
|
|
121
|
+
return max;
|
|
122
|
+
}
|
|
123
|
+
render(buffer, region) {
|
|
124
|
+
if (region.width <= 0 || region.height <= 0)
|
|
125
|
+
return;
|
|
126
|
+
const widths = this.allocate(region.width);
|
|
127
|
+
let col = region.col;
|
|
128
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
129
|
+
if (col >= region.col + region.width)
|
|
130
|
+
break; // 右侧溢出截断
|
|
131
|
+
if (widths[i] > 0) {
|
|
132
|
+
this.children[i].render(buffer, { row: region.row, col, width: widths[i], height: region.height });
|
|
133
|
+
}
|
|
134
|
+
col += widths[i];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** 内缩组合子:区域四向收缩后与原区域取交整夹,再整交子件 */
|
|
139
|
+
export class Inset {
|
|
140
|
+
child;
|
|
141
|
+
top;
|
|
142
|
+
right;
|
|
143
|
+
bottom;
|
|
144
|
+
left;
|
|
145
|
+
constructor(props) {
|
|
146
|
+
this.child = props.child;
|
|
147
|
+
this.top = props.top ?? 0;
|
|
148
|
+
this.right = props.right ?? 0;
|
|
149
|
+
this.bottom = props.bottom ?? 0;
|
|
150
|
+
this.left = props.left ?? 0;
|
|
151
|
+
}
|
|
152
|
+
measure(width) {
|
|
153
|
+
return Math.max(1, this.child.measure(Math.max(1, width - this.left - this.right))) + this.top + this.bottom;
|
|
154
|
+
}
|
|
155
|
+
render(buffer, region) {
|
|
156
|
+
const inner = {
|
|
157
|
+
row: region.row + this.top,
|
|
158
|
+
col: region.col + this.left,
|
|
159
|
+
width: Math.max(0, region.width - this.left - this.right),
|
|
160
|
+
height: Math.max(0, region.height - this.top - this.bottom),
|
|
161
|
+
};
|
|
162
|
+
const clipped = intersect(inner, region);
|
|
163
|
+
if (!clipped)
|
|
164
|
+
return;
|
|
165
|
+
this.child.render(buffer, clipped);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown 块解析件(07 §4.1 引擎节件 6——批 10d-4)。
|
|
3
|
+
*
|
|
4
|
+
* 块模型 = 滚动帽单位(呈现面件 1:一个 Markdown 块一子行——blockCount
|
|
5
|
+
* 即帽额度计数)。v1 支持面(CommonMark 子集,边界注释在案):
|
|
6
|
+
* 标题 #..######、段落(段内软换行折叠为空格——标准语义)、无序/有序
|
|
7
|
+
* 列表(嵌套缩进 2 空格一层)、围栏代码块(```/~~~)、引用(>,连续行
|
|
8
|
+
* 归块)、水平线(三连 - 或 * 或 _)。缩进四空格代码块、表格、脚注不
|
|
9
|
+
* 支撑(未列形按段落回退——坏输入不丢字)。
|
|
10
|
+
*/
|
|
11
|
+
import { parseInline } from './inline.js';
|
|
12
|
+
/** 围栏开行(```/~~~ 可 repetitions——闭栏同字符等长以上) */
|
|
13
|
+
const FENCE_RE = /^(`{3,}|~{3,})\s*(\S*)\s*$/;
|
|
14
|
+
/** 标题行(# 1-6 个 + 空格) */
|
|
15
|
+
const HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
16
|
+
/** 水平线(同字符 ≥3、容忍符间空格) */
|
|
17
|
+
const HR_RE = /^(?:[-*_])(?:\s*[-*_]){2,}$/;
|
|
18
|
+
/** 列表项(前导缩进 + 无序符或有序序号符 + 空格 + 内容) */
|
|
19
|
+
const LIST_RE = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/;
|
|
20
|
+
/**
|
|
21
|
+
* 块解析:全文 → 块序列。CRLF/CR 统一 LF;未闭围栏收至文末(防御——
|
|
22
|
+
* 流式半截文本换装前的定稿才进本件,但防御不依赖调用方)。
|
|
23
|
+
*/
|
|
24
|
+
export function parseMarkdown(text) {
|
|
25
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
26
|
+
const blocks = [];
|
|
27
|
+
let paragraph = [];
|
|
28
|
+
const flushParagraph = () => {
|
|
29
|
+
if (paragraph.length > 0) {
|
|
30
|
+
// 段内软换行折叠为空格(Markdown 标准语义——渲染时按宽重折)
|
|
31
|
+
blocks.push({ type: 'paragraph', spans: parseInline(paragraph.join(' ')) });
|
|
32
|
+
paragraph = [];
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < lines.length) {
|
|
37
|
+
const line = lines[i].trimEnd();
|
|
38
|
+
if (line.trim() === '') {
|
|
39
|
+
flushParagraph(); // 空行断段
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// 围栏代码块:吃到闭栏(同字符等长以上)或文末
|
|
44
|
+
const fence = FENCE_RE.exec(line.trim());
|
|
45
|
+
if (fence !== null) {
|
|
46
|
+
flushParagraph();
|
|
47
|
+
const marker = fence[1];
|
|
48
|
+
const language = fence[2] !== '' ? fence[2] : undefined;
|
|
49
|
+
const closeChar = marker[0];
|
|
50
|
+
const code = [];
|
|
51
|
+
i++;
|
|
52
|
+
while (i < lines.length) {
|
|
53
|
+
const inner = lines[i];
|
|
54
|
+
if (inner.trimStart().startsWith(closeChar) && inner.trim().length >= marker.length)
|
|
55
|
+
break;
|
|
56
|
+
code.push(inner);
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
i++; // 越过闭栏(文末防御——闭栏缺席时此步越界无害)
|
|
60
|
+
blocks.push({ type: 'code', lines: code, language });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// 标题
|
|
64
|
+
const heading = HEADING_RE.exec(line);
|
|
65
|
+
if (heading !== null) {
|
|
66
|
+
flushParagraph();
|
|
67
|
+
blocks.push({ type: 'heading', level: heading[1].length, spans: parseInline(heading[2]) });
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// 水平线
|
|
72
|
+
if (HR_RE.test(line.trim())) {
|
|
73
|
+
flushParagraph();
|
|
74
|
+
blocks.push({ type: 'hr' });
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// 引用(连续 > 行归一块)
|
|
79
|
+
if (line.startsWith('>')) {
|
|
80
|
+
flushParagraph();
|
|
81
|
+
const quoted = [];
|
|
82
|
+
while (i < lines.length && lines[i].startsWith('>')) {
|
|
83
|
+
// 剥 '> ' 或 '>' 前缀(无空格紧贴形同剥)
|
|
84
|
+
const body = lines[i].replace(/^>\s?/, '');
|
|
85
|
+
quoted.push(body.trim() === '' ? [] : parseInline(body));
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
blocks.push({ type: 'quote', lines: quoted });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// 列表项(每项一块——滚动帽子行单位)
|
|
92
|
+
const list = LIST_RE.exec(line);
|
|
93
|
+
if (list !== null) {
|
|
94
|
+
flushParagraph();
|
|
95
|
+
blocks.push({
|
|
96
|
+
type: 'list-item',
|
|
97
|
+
ordered: /\d/.test(list[2]),
|
|
98
|
+
marker: /\d/.test(list[2]) ? list[2] : '•',
|
|
99
|
+
indent: Math.floor(list[1].length / 2),
|
|
100
|
+
spans: parseInline(list[3]),
|
|
101
|
+
});
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// 其余 → 段落累积(相邻块型出现时由各自 flush 断段)
|
|
106
|
+
paragraph.push(line.trim());
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
flushParagraph();
|
|
110
|
+
return blocks;
|
|
111
|
+
}
|