@armadra/agent 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +93 -44
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +2 -1
- package/dist/agent/session-cache.js +1 -1
- package/dist/agent/session-classifier.d.ts +18 -0
- package/dist/agent/session-classifier.js +103 -0
- package/dist/agent/session-core.d.ts +13 -0
- package/dist/agent/session-tools.js +24 -3
- package/dist/agent/session.d.ts +3 -0
- package/dist/agent/session.js +12 -3
- package/dist/agent/tool-runner.js +15 -2
- package/dist/agent/types.d.ts +9 -1
- package/dist/ai/apis/anthropic-messages.js +3 -2
- package/dist/ai/apis/google-generative-ai.js +3 -2
- package/dist/ai/apis/openai-completions.js +3 -2
- package/dist/ai/apis/openai-responses.js +3 -2
- package/dist/ai/http.d.ts +28 -6
- package/dist/ai/http.js +41 -8
- package/dist/ai/providers/registry.d.ts +8 -1
- package/dist/ai/providers/registry.js +30 -22
- package/dist/ai/providers/suggest.d.ts +18 -0
- package/dist/ai/providers/suggest.js +72 -0
- package/dist/ai/sse.d.ts +6 -2
- package/dist/ai/sse.js +22 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +15279 -10151
- package/dist/cli/args.d.ts +19 -4
- package/dist/cli/args.js +83 -10
- package/dist/cli/bootstrap.js +27 -3
- package/dist/cli/codemode-notice.d.ts +20 -0
- package/dist/cli/codemode-notice.js +55 -0
- package/dist/cli/compose-session.d.ts +5 -0
- package/dist/cli/compose-session.js +32 -1
- package/dist/cli/compose-store.d.ts +1 -1
- package/dist/cli/compose-store.js +3 -1
- package/dist/cli/compose.d.ts +14 -4
- package/dist/cli/compose.js +29 -10
- package/dist/cli/default-model.d.ts +38 -1
- package/dist/cli/default-model.js +96 -9
- package/dist/cli/deps.d.ts +36 -0
- package/dist/cli/exit-codes.d.ts +2 -0
- package/dist/cli/exit-codes.js +3 -0
- package/dist/cli/fake-visibility.d.ts +13 -0
- package/dist/cli/fake-visibility.js +26 -0
- package/dist/cli/from-prompt.d.ts +18 -0
- package/dist/cli/from-prompt.js +49 -0
- package/dist/cli/main.d.ts +9 -2
- package/dist/cli/main.js +77 -3
- package/dist/cli/proxy.d.ts +51 -0
- package/dist/cli/proxy.js +135 -0
- package/dist/cli/startup-screen.d.ts +29 -0
- package/dist/cli/startup-screen.js +47 -0
- package/dist/cli/startup-steps.d.ts +1 -1
- package/dist/cli/startup-steps.js +14 -12
- package/dist/cli/subcommands/config.d.ts +19 -3
- package/dist/cli/subcommands/config.js +108 -21
- package/dist/cli/subcommands/context.js +4 -1
- package/dist/cli/subcommands/doctor.js +12 -1
- package/dist/cli/subcommands/init.js +2 -1
- package/dist/cli/subcommands/models-discover.d.ts +15 -9
- package/dist/cli/subcommands/models-discover.js +53 -46
- package/dist/cli/subcommands/probe-runner.d.ts +96 -0
- package/dist/cli/subcommands/probe-runner.js +264 -0
- package/dist/cli/subcommands/providers-probe.d.ts +34 -0
- package/dist/cli/subcommands/providers-probe.js +87 -0
- package/dist/cli/subcommands/providers.d.ts +3 -2
- package/dist/cli/subcommands/providers.js +48 -45
- package/dist/cli/subcommands/sessions-export.d.ts +10 -0
- package/dist/cli/subcommands/sessions-export.js +59 -0
- package/dist/cli/subcommands/sessions-search.d.ts +13 -0
- package/dist/cli/subcommands/sessions-search.js +103 -0
- package/dist/cli/subcommands/sessions.d.ts +3 -2
- package/dist/cli/subcommands/sessions.js +22 -1
- package/dist/cli/subcommands/stats.d.ts +17 -0
- package/dist/cli/subcommands/stats.js +198 -0
- package/dist/cli/system-prompt-arg.d.ts +11 -0
- package/dist/cli/system-prompt-arg.js +34 -0
- package/dist/codemode/modes.d.ts +4 -11
- package/dist/codemode/modes.js +5 -27
- package/dist/codemode/tool.d.ts +18 -14
- package/dist/codemode/tool.js +71 -28
- package/dist/config/init.d.ts +6 -2
- package/dist/config/init.js +12 -5
- package/dist/config/json-schema.d.ts +1 -0
- package/dist/config/json-schema.js +36 -3
- package/dist/config/key-docs.d.ts +22 -0
- package/dist/config/key-docs.js +105 -0
- package/dist/config/merge.d.ts +9 -8
- package/dist/config/merge.js +22 -7
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +24 -5
- package/dist/config/types.d.ts +34 -6
- package/dist/config/types.js +17 -1
- package/dist/modes/commands-core.js +6 -5
- package/dist/modes/interactive/approval-dialog.d.ts +21 -5
- package/dist/modes/interactive/approval-dialog.js +106 -27
- package/dist/modes/interactive/commands.d.ts +11 -2
- package/dist/modes/interactive/commands.js +56 -16
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +65 -81
- package/dist/modes/interactive/key-dispatch.d.ts +3 -1
- package/dist/modes/interactive/key-dispatch.js +5 -6
- package/dist/modes/interactive/line/line-mode.d.ts +1 -0
- package/dist/modes/interactive/line/line-mode.js +6 -4
- package/dist/modes/interactive/line/line-render.d.ts +4 -0
- package/dist/modes/interactive/line/line-render.js +28 -6
- package/dist/modes/interactive/message-view.d.ts +48 -9
- package/dist/modes/interactive/message-view.js +238 -44
- package/dist/modes/interactive/panels.d.ts +18 -0
- package/dist/modes/interactive/panels.js +143 -0
- package/dist/modes/interactive/pickers.d.ts +23 -2
- package/dist/modes/interactive/pickers.js +48 -15
- package/dist/modes/interactive/run-indicator.d.ts +51 -0
- package/dist/modes/interactive/run-indicator.js +189 -0
- package/dist/modes/interactive/startup-header.d.ts +40 -0
- package/dist/modes/interactive/startup-header.js +169 -0
- package/dist/modes/interactive/status-bar.d.ts +18 -15
- package/dist/modes/interactive/status-bar.js +98 -56
- package/dist/modes/interactive/tool-summary.d.ts +46 -0
- package/dist/modes/interactive/tool-summary.js +218 -0
- package/dist/modes/interactive/tool-view.d.ts +48 -15
- package/dist/modes/interactive/tool-view.js +203 -145
- package/dist/modes/print/print-mode.d.ts +31 -4
- package/dist/modes/print/print-mode.js +116 -7
- package/dist/modes/rpc/commands.js +5 -0
- package/dist/permissions/auto-safe.d.ts +60 -0
- package/dist/permissions/auto-safe.js +529 -0
- package/dist/permissions/classifier.d.ts +64 -0
- package/dist/permissions/classifier.js +184 -0
- package/dist/permissions/dangerous.d.ts +5 -0
- package/dist/permissions/dangerous.js +1 -1
- package/dist/permissions/modes.d.ts +30 -0
- package/dist/permissions/modes.js +78 -0
- package/dist/permissions/pipeline.d.ts +31 -4
- package/dist/permissions/pipeline.js +196 -6
- package/dist/permissions/protected.d.ts +19 -0
- package/dist/permissions/protected.js +74 -0
- package/dist/permissions/rules.js +3 -0
- package/dist/permissions/types.d.ts +50 -3
- package/dist/permissions/types.js +3 -0
- package/dist/sdk.d.ts +9 -3
- package/dist/sdk.js +10 -2
- package/dist/session/export.d.ts +32 -0
- package/dist/session/export.js +187 -0
- package/dist/session/redact.d.ts +15 -0
- package/dist/session/redact.js +55 -0
- package/dist/session/reuse.d.ts +33 -0
- package/dist/session/reuse.js +86 -0
- package/dist/session/scan.d.ts +34 -0
- package/dist/session/scan.js +140 -0
- package/dist/session/search.d.ts +52 -0
- package/dist/session/search.js +211 -0
- package/dist/session/stats-aggregate.d.ts +63 -0
- package/dist/session/stats-aggregate.js +163 -0
- package/dist/session/stats-index.d.ts +26 -0
- package/dist/session/stats-index.js +91 -0
- package/dist/session/stats-scan.d.ts +54 -0
- package/dist/session/stats-scan.js +236 -0
- package/dist/tools/presets.d.ts +35 -8
- package/dist/tools/presets.js +56 -17
- package/dist/tui/component.d.ts +8 -2
- package/dist/tui/component.js +3 -1
- package/dist/tui/components/box.d.ts +6 -1
- package/dist/tui/components/box.js +16 -6
- package/dist/tui/components/card.d.ts +23 -0
- package/dist/tui/components/card.js +37 -0
- package/dist/tui/components/editor-history.d.ts +6 -0
- package/dist/tui/components/editor-history.js +45 -0
- package/dist/tui/components/editor-paste.d.ts +1 -1
- package/dist/tui/components/editor-paste.js +4 -4
- package/dist/tui/components/editor.d.ts +11 -5
- package/dist/tui/components/editor.js +52 -58
- package/dist/tui/components/key-value.d.ts +3 -0
- package/dist/tui/components/key-value.js +16 -6
- package/dist/tui/components/loader.d.ts +37 -7
- package/dist/tui/components/loader.js +84 -21
- package/dist/tui/components/markdown.d.ts +5 -1
- package/dist/tui/components/markdown.js +45 -21
- package/dist/tui/components/meter.d.ts +3 -3
- package/dist/tui/components/meter.js +13 -11
- package/dist/tui/components/select-list.d.ts +23 -1
- package/dist/tui/components/select-list.js +76 -13
- package/dist/tui/glyphs.d.ts +68 -0
- package/dist/tui/glyphs.js +114 -0
- package/dist/tui/theme.d.ts +30 -6
- package/dist/tui/theme.js +103 -17
- package/dist/tui.d.ts +4 -2
- package/dist/tui.js +3 -1
- package/docs/codemode.md +23 -9
- package/docs/hooks.md +10 -10
- package/docs/permissions.md +148 -0
- package/docs/providers.md +33 -10
- package/docs/rpc.md +36 -36
- package/docs/session-format.md +2 -1
- package/docs/sessions.md +134 -0
- package/docs/tui.md +139 -62
- package/package.json +3 -1
package/dist/tools/presets.js
CHANGED
|
@@ -5,34 +5,68 @@
|
|
|
5
5
|
* | ------------- | ----------------------------------------- |
|
|
6
6
|
* | `default` | read、edit、write、bash、grep、glob |
|
|
7
7
|
* | `minimal` | read、edit、write、bash |
|
|
8
|
-
* | `codemode`
|
|
8
|
+
* | `codemode-only` | 只有 codemode(其余工具在脚本里调用) |
|
|
9
9
|
* | `coordinator` | read + 宿主注册的工具 |
|
|
10
10
|
*
|
|
11
11
|
* - 预设只管**内置**工具;宿主 / SDK 注册的工具(`canvas_*` 等)总在活动集里,除非用户
|
|
12
12
|
* `--tools a,b,c` 整组替换(之后只用列出的名字)。
|
|
13
13
|
* - `tools.default`:`+name` / `-name` 在预设上增减;不带前缀的名字整组替换预设的内置工具,
|
|
14
14
|
* 之后再应用带前缀的项。未注册的名字记 warning 并忽略。
|
|
15
|
-
* - codemode
|
|
16
|
-
* - codemode
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* - `codemode` 是 `codemode-only` 的旧名(0.3.0),配置合并与命令行解析时折成规范名。
|
|
16
|
+
* - codemode 开关跟随预设(`presetCodemodeMode`):`default` → `on`(只在沙箱 strict,即 Node ≥ 25 隔离
|
|
17
|
+
* 网络时;Node 22 / 24 → `off`),`codemode-only` → `only`,`minimal` / `coordinator` → `off`。
|
|
18
|
+
* `codemode.mode` 显式配置覆盖(缺省配置不写这个键,以后调整映射能惠及老用户)。
|
|
19
|
+
* - `coordinator` 预设即使显式 `on`,脚本里能调用的工具也只限活动集(read + 宿主工具),见
|
|
20
|
+
* `codemodeCallableFilter`:协调者「不写文件、不跑 bash」的约定不能经 codemode 绕过。
|
|
21
|
+
* - codemode 工具由 B10 经组装根的 `toolFactories` 注册(生效模式为 off 时不注册);不可用
|
|
22
|
+
* (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode-only 预设**回退到 default 并
|
|
23
|
+
* warning**(不报错:零配置用户不应因此起不来),显式 `on` 同样忽略;跟随预设得到的 `on` 静默回退。
|
|
19
24
|
* - `only` 模式活动集独占:宿主 / SDK 工具也不直接暴露,只能在脚本里调用(`exclusive`)。
|
|
20
25
|
*/
|
|
26
|
+
import { detectSandboxCapability } from "../codemode/capability.js";
|
|
27
|
+
import { canonicalPreset, } from "../config/types.js";
|
|
21
28
|
import { ToolRegistry } from "./registry.js";
|
|
22
29
|
export const CODEMODE_TOOL = "codemode";
|
|
23
|
-
/** 各预设下模型直接看到的内置工具(codemode 预设见 `resolvePreset`)。 */
|
|
30
|
+
/** 各预设下模型直接看到的内置工具(codemode-only 预设见 `resolvePreset`)。 */
|
|
24
31
|
export const PRESET_TOOLS = Object.freeze({
|
|
25
32
|
default: ["bash", "edit", "glob", "grep", "read", "write"],
|
|
26
33
|
minimal: ["bash", "edit", "read", "write"],
|
|
27
|
-
codemode: [CODEMODE_TOOL],
|
|
34
|
+
"codemode-only": [CODEMODE_TOOL],
|
|
28
35
|
coordinator: ["read"],
|
|
29
36
|
});
|
|
30
|
-
/**
|
|
31
|
-
|
|
37
|
+
/**
|
|
38
|
+
* 预设对应的 codemode 模式(没有显式 `codemode.mode` 时)。`default` 只在沙箱 strict(Node ≥ 25,
|
|
39
|
+
* 权限模型同时隔离网络)时开 `on`:Node 22 / 24 下 codemode 属 execute 类,每次都要审批,`-p` 下
|
|
40
|
+
* 直接被拒,所以缺省关闭(启动时提示一次,见 cli/codemode-notice.ts)。
|
|
41
|
+
*/
|
|
42
|
+
export function presetCodemodeMode(preset, strict) {
|
|
43
|
+
switch (preset) {
|
|
44
|
+
case "codemode-only":
|
|
45
|
+
return "only";
|
|
46
|
+
case "default":
|
|
47
|
+
return strict ? "on" : "off";
|
|
48
|
+
default:
|
|
49
|
+
return "off";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** 生效的 codemode 模式及其来源;`strict` 缺省按运行时探测(测试与组装根可注入)。 */
|
|
53
|
+
export function resolveCodemodeMode(config, strict = detectSandboxCapability().strict) {
|
|
54
|
+
const preset = canonicalPreset(config.tools?.preset) ?? "default";
|
|
32
55
|
const explicit = config.codemode?.mode;
|
|
33
56
|
if (explicit !== undefined)
|
|
34
|
-
return explicit;
|
|
35
|
-
return
|
|
57
|
+
return { mode: explicit, source: "config", preset, strict };
|
|
58
|
+
return { mode: presetCodemodeMode(preset, strict), source: "preset", preset, strict };
|
|
59
|
+
}
|
|
60
|
+
/** 生效的 codemode 模式:显式配置优先,否则跟随预设。 */
|
|
61
|
+
export function effectiveCodemodeMode(config, strict) {
|
|
62
|
+
return resolveCodemodeMode(config, strict).mode;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* codemode 脚本里可调用工具的范围:`coordinator` 预设只能调活动集里的工具(`"active"`),其它预设
|
|
66
|
+
* 不限(`undefined`;`on` 模式下脚本还能调没直接暴露的 ls / task / todo 等)。
|
|
67
|
+
*/
|
|
68
|
+
export function codemodeCallableFilter(config) {
|
|
69
|
+
return canonicalPreset(config.tools?.preset) === "coordinator" ? "active" : undefined;
|
|
36
70
|
}
|
|
37
71
|
/**
|
|
38
72
|
* `tools.default` 微调:不带前缀的名字整组替换 `base`,再依次应用 `+name` / `-name`。
|
|
@@ -55,13 +89,18 @@ export function applyToolAdjustments(base, adjustments) {
|
|
|
55
89
|
}
|
|
56
90
|
export function resolvePreset(input) {
|
|
57
91
|
const warnings = [];
|
|
58
|
-
|
|
59
|
-
let
|
|
92
|
+
const resolved = resolveCodemodeMode(input.config, input.strict);
|
|
93
|
+
let preset = resolved.preset;
|
|
94
|
+
let codemode = resolved.mode;
|
|
95
|
+
// 跟随预设得到的 on(default 预设)在 codemode 不可用时静默回退:零配置不该因此出 warning。
|
|
96
|
+
if (codemode === "on" && resolved.source === "preset" && !input.available(CODEMODE_TOOL)) {
|
|
97
|
+
codemode = "off";
|
|
98
|
+
}
|
|
60
99
|
if (codemode !== "off" && !input.available(CODEMODE_TOOL)) {
|
|
61
|
-
warnings.push(preset === "codemode"
|
|
62
|
-
? "工具预设 codemode 需要 codemode 工具(不可用),已回退到 default"
|
|
100
|
+
warnings.push(preset === "codemode-only"
|
|
101
|
+
? "工具预设 codemode-only 需要 codemode 工具(不可用),已回退到 default"
|
|
63
102
|
: `codemode.mode ${codemode} 需要 codemode 工具(不可用),已忽略`);
|
|
64
|
-
if (preset === "codemode")
|
|
103
|
+
if (preset === "codemode-only")
|
|
65
104
|
preset = "default";
|
|
66
105
|
codemode = "off";
|
|
67
106
|
}
|
|
@@ -69,7 +108,7 @@ export function resolvePreset(input) {
|
|
|
69
108
|
if (codemode === "only")
|
|
70
109
|
base = [CODEMODE_TOOL];
|
|
71
110
|
else {
|
|
72
|
-
base = [...PRESET_TOOLS[preset === "codemode" ? "default" : preset]];
|
|
111
|
+
base = [...PRESET_TOOLS[preset === "codemode-only" ? "default" : preset]];
|
|
73
112
|
if (codemode === "on")
|
|
74
113
|
base.push(CODEMODE_TOOL);
|
|
75
114
|
}
|
package/dist/tui/component.d.ts
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 补全与偏差:
|
|
5
5
|
* - 设计的 `Theme` 写作 `bg(...)` 与 `bold/dim/italic/underline(s)`,此处展开为完整签名。
|
|
6
|
-
* - `SemanticColor` 取 §12.7 的 11
|
|
6
|
+
* - `SemanticColor` 取 §12.7 的 11 个语义色名;终端界面视觉设计 v1 加 `muted`(正文与 dim 之间的次要文字)、
|
|
7
|
+
* `link`(链接文字)、`selection`(选中行底色,只在 ≥ 256 色时作 bg)共 14 个。
|
|
7
8
|
* - 补全 `CURSOR_MARKER` 常量与 `isFocusable()` 判别函数(B4 的 tui.ts 与 B7 都要用)。
|
|
9
|
+
* - 终端界面视觉设计 v1:`Theme.glyphs`(字形表与 ASCII 回退,见 glyphs.ts)。
|
|
8
10
|
*/
|
|
11
|
+
import type { Glyphs } from "./glyphs.js";
|
|
12
|
+
export type { Glyphs, BoxGlyphs } from "./glyphs.js";
|
|
9
13
|
/** APC 序列;获焦组件在光标处输出它,TUI 据此摆硬件光标并在输出前剔除。 */
|
|
10
14
|
export declare const CURSOR_MARKER = "\u001B_ama:c\u0007";
|
|
11
15
|
export interface Component {
|
|
@@ -20,7 +24,7 @@ export interface Focusable {
|
|
|
20
24
|
focused: boolean;
|
|
21
25
|
}
|
|
22
26
|
export declare function isFocusable(component: Component): component is Component & Focusable;
|
|
23
|
-
export type SemanticColor = "text" | "dim" | "accent" | "success" | "warning" | "error" | "user" | "assistant" | "tool" | "border" | "code";
|
|
27
|
+
export type SemanticColor = "text" | "dim" | "accent" | "success" | "warning" | "error" | "user" | "assistant" | "tool" | "border" | "code" | "muted" | "link" | "selection";
|
|
24
28
|
/** 0 = 无色(NO_COLOR);16_777_216 = truecolor。 */
|
|
25
29
|
export type ColorDepth = 0 | 16 | 256 | 16_777_216;
|
|
26
30
|
export interface ThemeCapabilities {
|
|
@@ -35,4 +39,6 @@ export interface Theme {
|
|
|
35
39
|
italic(text: string): string;
|
|
36
40
|
underline(text: string): string;
|
|
37
41
|
readonly caps: ThemeCapabilities;
|
|
42
|
+
/** 字形(Unicode 或 ASCII 回退);组件一律从这里取。 */
|
|
43
|
+
readonly glyphs: Glyphs;
|
|
38
44
|
}
|
package/dist/tui/component.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 补全与偏差:
|
|
5
5
|
* - 设计的 `Theme` 写作 `bg(...)` 与 `bold/dim/italic/underline(s)`,此处展开为完整签名。
|
|
6
|
-
* - `SemanticColor` 取 §12.7 的 11
|
|
6
|
+
* - `SemanticColor` 取 §12.7 的 11 个语义色名;终端界面视觉设计 v1 加 `muted`(正文与 dim 之间的次要文字)、
|
|
7
|
+
* `link`(链接文字)、`selection`(选中行底色,只在 ≥ 256 色时作 bg)共 14 个。
|
|
7
8
|
* - 补全 `CURSOR_MARKER` 常量与 `isFocusable()` 判别函数(B4 的 tui.ts 与 B7 都要用)。
|
|
9
|
+
* - 终端界面视觉设计 v1:`Theme.glyphs`(字形表与 ASCII 回退,见 glyphs.ts)。
|
|
8
10
|
*/
|
|
9
11
|
/** APC 序列;获焦组件在光标处输出它,TUI 据此摆硬件光标并在输出前剔除。 */
|
|
10
12
|
export const CURSOR_MARKER = "\x1b_ama:c\x07";
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* 边框 / 内边距容器(设计 §12.1)。[B4]
|
|
3
3
|
*
|
|
4
4
|
* 子组件按内宽渲染后加圆角边框(可选标题,嵌在上边框)与内边距;宽度不足 4 列时退化为只加内边距。
|
|
5
|
+
* 边框字形取 `theme.glyphs.box`(ASCII 下为 `+ - |`),颜色缺省 `border`,可用 `borderColor` 改
|
|
6
|
+
* (审批对话框按严重度用 error / warning)。
|
|
5
7
|
* 焦点与键输入透传给子组件,所以可以直接把 `Box(SelectList)` 交给 `showOverlay`。
|
|
6
8
|
*/
|
|
7
|
-
import { type Component, type Theme } from "../component.js";
|
|
9
|
+
import { type Component, type SemanticColor, type Theme } from "../component.js";
|
|
8
10
|
export interface BoxOptions {
|
|
9
11
|
border?: boolean;
|
|
10
12
|
paddingX?: number;
|
|
11
13
|
paddingY?: number;
|
|
12
14
|
title?: string;
|
|
13
15
|
theme?: Theme;
|
|
16
|
+
/** 边框颜色,缺省 `border`。 */
|
|
17
|
+
borderColor?: SemanticColor;
|
|
14
18
|
}
|
|
15
19
|
export declare class Box implements Component {
|
|
16
20
|
readonly child: Component;
|
|
@@ -21,6 +25,7 @@ export declare class Box implements Component {
|
|
|
21
25
|
set focused(value: boolean);
|
|
22
26
|
handleInput(data: string): void;
|
|
23
27
|
render(width: number): string[];
|
|
28
|
+
private get glyphs();
|
|
24
29
|
private topBorder;
|
|
25
30
|
invalidate(): void;
|
|
26
31
|
}
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* 边框 / 内边距容器(设计 §12.1)。[B4]
|
|
3
3
|
*
|
|
4
4
|
* 子组件按内宽渲染后加圆角边框(可选标题,嵌在上边框)与内边距;宽度不足 4 列时退化为只加内边距。
|
|
5
|
+
* 边框字形取 `theme.glyphs.box`(ASCII 下为 `+ - |`),颜色缺省 `border`,可用 `borderColor` 改
|
|
6
|
+
* (审批对话框按严重度用 error / warning)。
|
|
5
7
|
* 焦点与键输入透传给子组件,所以可以直接把 `Box(SelectList)` 交给 `showOverlay`。
|
|
6
8
|
*/
|
|
7
9
|
import { isFocusable } from "../component.js";
|
|
8
10
|
import { padToWidth, truncateToWidth, visibleWidth } from "../ansi.js";
|
|
11
|
+
import { UNICODE_GLYPHS } from "../glyphs.js";
|
|
9
12
|
export class Box {
|
|
10
13
|
child;
|
|
11
14
|
options;
|
|
@@ -31,7 +34,9 @@ export class Box {
|
|
|
31
34
|
const frame = border ? 2 : 0;
|
|
32
35
|
const inner = Math.max(1, width - frame - padX * 2);
|
|
33
36
|
const body = this.child.render(inner);
|
|
34
|
-
const
|
|
37
|
+
const borderColor = this.options.borderColor ?? "border";
|
|
38
|
+
const color = (s) => this.options.theme ? this.options.theme.fg(borderColor, s) : s;
|
|
39
|
+
const g = this.glyphs;
|
|
35
40
|
const pad = " ".repeat(padX);
|
|
36
41
|
const contentWidth = Math.max(0, width - frame);
|
|
37
42
|
const rows = [];
|
|
@@ -47,17 +52,22 @@ export class Box {
|
|
|
47
52
|
return rows;
|
|
48
53
|
const lines = [this.topBorder(width, color)];
|
|
49
54
|
for (const row of rows)
|
|
50
|
-
lines.push(color(
|
|
51
|
-
lines.push(color(
|
|
55
|
+
lines.push(color(g.vertical) + row + color(g.vertical));
|
|
56
|
+
lines.push(color(g.bottomLeft + g.horizontal.repeat(width - 2) + g.bottomRight));
|
|
52
57
|
return lines;
|
|
53
58
|
}
|
|
59
|
+
get glyphs() {
|
|
60
|
+
return (this.options.theme?.glyphs ?? UNICODE_GLYPHS).box;
|
|
61
|
+
}
|
|
54
62
|
topBorder(width, color) {
|
|
63
|
+
const g = this.glyphs;
|
|
55
64
|
const title = this.options.title;
|
|
56
|
-
if (title === undefined || title === "")
|
|
57
|
-
return color(
|
|
65
|
+
if (title === undefined || title === "") {
|
|
66
|
+
return color(g.topLeft + g.horizontal.repeat(width - 2) + g.topRight);
|
|
67
|
+
}
|
|
58
68
|
const label = truncateToWidth(` ${title} `, width - 4);
|
|
59
69
|
const rest = Math.max(0, width - 3 - visibleWidth(label));
|
|
60
|
-
return color(
|
|
70
|
+
return color(g.topLeft + g.horizontal) + label + color(g.horizontal.repeat(rest) + g.topRight);
|
|
61
71
|
}
|
|
62
72
|
invalidate() {
|
|
63
73
|
this.child.invalidate();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 左竖条卡片(终端界面视觉设计 v1 §3.6、§3.13):压缩摘要、`/session` 等面板。
|
|
3
|
+
*
|
|
4
|
+
* 每行前加 `theme.glyphs.card`(`▎`,ASCII `|`)与一个空格,竖条用 `border` 色;可选标题行(正文色粗体)
|
|
5
|
+
* 与跟在标题后的副标题(`muted`,两个空格隔开)。不加上下边框——Box 只留给代码块、启动头与覆盖层。
|
|
6
|
+
* 子组件按 `width - 2` 渲染;空行只画竖条。
|
|
7
|
+
*/
|
|
8
|
+
import type { Component, SemanticColor, Theme } from "../component.js";
|
|
9
|
+
export interface CardOptions {
|
|
10
|
+
theme?: Theme;
|
|
11
|
+
title?: string;
|
|
12
|
+
/** 标题后的次要说明(muted)。 */
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
/** 竖条颜色,缺省 `border`。 */
|
|
15
|
+
barColor?: SemanticColor;
|
|
16
|
+
}
|
|
17
|
+
export declare class Card implements Component {
|
|
18
|
+
readonly child: Component | undefined;
|
|
19
|
+
private readonly options;
|
|
20
|
+
constructor(child: Component | undefined, options?: CardOptions);
|
|
21
|
+
render(width: number): string[];
|
|
22
|
+
invalidate(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 左竖条卡片(终端界面视觉设计 v1 §3.6、§3.13):压缩摘要、`/session` 等面板。
|
|
3
|
+
*
|
|
4
|
+
* 每行前加 `theme.glyphs.card`(`▎`,ASCII `|`)与一个空格,竖条用 `border` 色;可选标题行(正文色粗体)
|
|
5
|
+
* 与跟在标题后的副标题(`muted`,两个空格隔开)。不加上下边框——Box 只留给代码块、启动头与覆盖层。
|
|
6
|
+
* 子组件按 `width - 2` 渲染;空行只画竖条。
|
|
7
|
+
*/
|
|
8
|
+
import { truncateToWidth } from "../ansi.js";
|
|
9
|
+
import { UNICODE_GLYPHS } from "../glyphs.js";
|
|
10
|
+
export class Card {
|
|
11
|
+
child;
|
|
12
|
+
options;
|
|
13
|
+
constructor(child, options = {}) {
|
|
14
|
+
this.child = child;
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
render(width) {
|
|
18
|
+
const { theme, title, subtitle } = this.options;
|
|
19
|
+
const glyph = (theme?.glyphs ?? UNICODE_GLYPHS).card;
|
|
20
|
+
const bar = theme ? theme.fg(this.options.barColor ?? "border", glyph) : glyph;
|
|
21
|
+
const inner = Math.max(1, width - 2);
|
|
22
|
+
const lines = [];
|
|
23
|
+
if (title !== undefined && title !== "") {
|
|
24
|
+
let head = theme ? theme.bold(title) : title;
|
|
25
|
+
if (subtitle !== undefined && subtitle !== "") {
|
|
26
|
+
head += " " + (theme ? theme.fg("muted", subtitle) : subtitle);
|
|
27
|
+
}
|
|
28
|
+
lines.push(head);
|
|
29
|
+
}
|
|
30
|
+
if (this.child)
|
|
31
|
+
lines.push(...this.child.render(inner));
|
|
32
|
+
return lines.map((line) => line === "" ? bar : truncateToWidth(`${bar} ${truncateToWidth(line, inner)}`, width));
|
|
33
|
+
}
|
|
34
|
+
invalidate() {
|
|
35
|
+
this.child?.invalidate();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编辑器的历史文件(设计 §12.4):JSONL,每行一个 JSON 字符串,保留最近 `limit` 条。[B4]
|
|
3
|
+
* 读写失败都不影响编辑(损坏的行跳过;写失败静默)。
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadHistoryFile(path: string, limit?: number): string[];
|
|
6
|
+
export declare function appendHistoryFile(path: string, entry: string, limit: number): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编辑器的历史文件(设计 §12.4):JSONL,每行一个 JSON 字符串,保留最近 `limit` 条。[B4]
|
|
3
|
+
* 读写失败都不影响编辑(损坏的行跳过;写失败静默)。
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
export function loadHistoryFile(path, limit = 500) {
|
|
8
|
+
let text;
|
|
9
|
+
try {
|
|
10
|
+
text = readFileSync(path, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const entries = [];
|
|
16
|
+
for (const line of text.split("\n")) {
|
|
17
|
+
if (line.trim() === "")
|
|
18
|
+
continue;
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(line);
|
|
21
|
+
if (typeof value === "string" && value !== "")
|
|
22
|
+
entries.push(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// 损坏的行跳过
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return entries.slice(-limit);
|
|
29
|
+
}
|
|
30
|
+
export function appendHistoryFile(path, entry, limit) {
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
34
|
+
const all = loadHistoryFile(path, Number.MAX_SAFE_INTEGER);
|
|
35
|
+
if (all.length > limit * 2) {
|
|
36
|
+
writeFileSync(path, all
|
|
37
|
+
.slice(-limit)
|
|
38
|
+
.map((e) => JSON.stringify(e) + "\n")
|
|
39
|
+
.join(""));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// 历史写失败不影响编辑
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 大粘贴折叠(设计 §12.4):超过 10 行或超过 1 000 字符的粘贴折叠为 `[
|
|
2
|
+
* 大粘贴折叠(设计 §12.4):超过 10 行或超过 1 000 字符的粘贴折叠为 `[粘贴 #N · M 行]`,
|
|
3
3
|
* 在编辑器里是不可分割段;提交时展开为原文。[B4]
|
|
4
4
|
*
|
|
5
5
|
* - 换行规范化:`\r\n` / 单独的 `\r` → `\n`(括号粘贴里终端常把换行发成 `\r`)。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export const PASTE_LINE_THRESHOLD = 10;
|
|
10
10
|
export const PASTE_CHAR_THRESHOLD = 1000;
|
|
11
|
-
const MARKER_RE = /\[
|
|
11
|
+
const MARKER_RE = /\[粘贴 #(\d+) · (\d+) 行\]/g;
|
|
12
12
|
export function normalizePastedText(text) {
|
|
13
13
|
return text.replace(/\r\n?/g, "\n");
|
|
14
14
|
}
|
|
@@ -20,7 +20,7 @@ export function shouldCollapse(text) {
|
|
|
20
20
|
return countLines(text) > PASTE_LINE_THRESHOLD || text.length > PASTE_CHAR_THRESHOLD;
|
|
21
21
|
}
|
|
22
22
|
export function formatMarker(id, lines) {
|
|
23
|
-
return `[
|
|
23
|
+
return `[粘贴 #${id} · ${lines} 行]`;
|
|
24
24
|
}
|
|
25
25
|
export class PasteStore {
|
|
26
26
|
next = 1;
|
|
@@ -48,7 +48,7 @@ export class PasteStore {
|
|
|
48
48
|
}
|
|
49
49
|
/** 一行内已登记标记的区间(编辑器的不可分割段)。 */
|
|
50
50
|
ranges(line) {
|
|
51
|
-
if (this.entries.size === 0 || !line.includes("[
|
|
51
|
+
if (this.entries.size === 0 || !line.includes("[粘贴 #"))
|
|
52
52
|
return [];
|
|
53
53
|
const out = [];
|
|
54
54
|
for (const m of line.matchAll(MARKER_RE)) {
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 多行编辑器组件(设计 §12.3、§12.4)。[B4]
|
|
3
3
|
*
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
4
|
+
* - 渲染:上下规则线之间按列宽折行(不切开字素与折叠标记);首行前是提示符 `› `(`user` 粗体,
|
|
5
|
+
* `disableSubmit` 时 dim),续行缩进 2 列对齐;超过 `maxVisibleLines` 时视窗跟随光标,规则线上提示
|
|
6
|
+
* 上 / 下方隐藏的行数(dim);获焦时在光标处输出 CURSOR_MARKER 并反显光标格。空文本显示占位
|
|
7
|
+
* (获焦时也显示,光标反显占位首字)。
|
|
6
8
|
* - 键位:`tui.editor.*`(见 keybindings.ts)。Enter 提交(`disableSubmit` 时忽略);Shift+Enter / Ctrl+J 换行;
|
|
7
9
|
* Up/Down 在首 / 末视觉行时浏览历史(单行文本或空文本或已在浏览中),否则按视觉行移动。
|
|
8
10
|
* - 括号粘贴(`ESC[200~…ESC[201~`,由 StdinBuffer 合成一次输入):> 10 行或 > 1 000 字符折叠为
|
|
9
|
-
* `[
|
|
10
|
-
* - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7
|
|
11
|
+
* `[粘贴 #N · M 行]`,提交时展开;粘贴期间不触发补全;粘贴后紧随的 `\r` 是普通 Enter(提交)。
|
|
12
|
+
* - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7 实现);同步或异步返回候选与替换起点;
|
|
13
|
+
* 弹层缩进 2 列,尾行 `(i/n) Tab 接受 · Esc 关闭`。
|
|
11
14
|
* - 历史:会话内 + 可选历史文件(JSONL,每行一个 JSON 字符串,保留最近 500 条)。
|
|
12
15
|
*/
|
|
13
16
|
import { type Component, type Focusable, type Theme } from "../component.js";
|
|
14
17
|
import { type Keybindings } from "../keybindings.js";
|
|
18
|
+
export { loadHistoryFile } from "./editor-history.js";
|
|
15
19
|
export interface AutocompleteItem {
|
|
16
20
|
/** 接受后替换进编辑器的文本。 */
|
|
17
21
|
value: string;
|
|
@@ -52,7 +56,6 @@ export interface EditorOptions {
|
|
|
52
56
|
onSubmit?(text: string): void;
|
|
53
57
|
onChange?(text: string): void;
|
|
54
58
|
}
|
|
55
|
-
export declare function loadHistoryFile(path: string, limit?: number): string[];
|
|
56
59
|
export declare class Editor implements Component, Focusable {
|
|
57
60
|
private readonly options;
|
|
58
61
|
focused: boolean;
|
|
@@ -103,7 +106,10 @@ export declare class Editor implements Component, Focusable {
|
|
|
103
106
|
private applyCompletion;
|
|
104
107
|
private acceptCompletion;
|
|
105
108
|
private closeCompletion;
|
|
109
|
+
/** 文本列宽:去掉提示符与行尾光标格。 */
|
|
106
110
|
private contentWidth;
|
|
111
|
+
private prompt;
|
|
112
|
+
private placeholderLine;
|
|
107
113
|
/** 逻辑行 → 视觉行(按列宽折行,不切开字素与折叠标记)。 */
|
|
108
114
|
private layout;
|
|
109
115
|
private cursorRowIndex;
|
|
@@ -1,65 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 多行编辑器组件(设计 §12.3、§12.4)。[B4]
|
|
3
3
|
*
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
4
|
+
* - 渲染:上下规则线之间按列宽折行(不切开字素与折叠标记);首行前是提示符 `› `(`user` 粗体,
|
|
5
|
+
* `disableSubmit` 时 dim),续行缩进 2 列对齐;超过 `maxVisibleLines` 时视窗跟随光标,规则线上提示
|
|
6
|
+
* 上 / 下方隐藏的行数(dim);获焦时在光标处输出 CURSOR_MARKER 并反显光标格。空文本显示占位
|
|
7
|
+
* (获焦时也显示,光标反显占位首字)。
|
|
6
8
|
* - 键位:`tui.editor.*`(见 keybindings.ts)。Enter 提交(`disableSubmit` 时忽略);Shift+Enter / Ctrl+J 换行;
|
|
7
9
|
* Up/Down 在首 / 末视觉行时浏览历史(单行文本或空文本或已在浏览中),否则按视觉行移动。
|
|
8
10
|
* - 括号粘贴(`ESC[200~…ESC[201~`,由 StdinBuffer 合成一次输入):> 10 行或 > 1 000 字符折叠为
|
|
9
|
-
* `[
|
|
10
|
-
* - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7
|
|
11
|
+
* `[粘贴 #N · M 行]`,提交时展开;粘贴期间不触发补全;粘贴后紧随的 `\r` 是普通 Enter(提交)。
|
|
12
|
+
* - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7 实现);同步或异步返回候选与替换起点;
|
|
13
|
+
* 弹层缩进 2 列,尾行 `(i/n) Tab 接受 · Esc 关闭`。
|
|
11
14
|
* - 历史:会话内 + 可选历史文件(JSONL,每行一个 JSON 字符串,保留最近 500 条)。
|
|
12
15
|
*/
|
|
13
|
-
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { dirname } from "node:path";
|
|
15
16
|
import { CURSOR_MARKER } from "../component.js";
|
|
16
17
|
import { truncateToWidth, visibleWidth } from "../ansi.js";
|
|
17
18
|
import { defaultKeybindings } from "../keybindings.js";
|
|
18
19
|
import { isPasteData, isPrintableText, unwrapPaste } from "../keys.js";
|
|
19
20
|
import { plainTheme } from "../theme.js";
|
|
20
21
|
import { EditorBuffer } from "./editor-buffer.js";
|
|
22
|
+
import { appendHistoryFile, loadHistoryFile } from "./editor-history.js";
|
|
21
23
|
import { PasteStore } from "./editor-paste.js";
|
|
22
24
|
import { SelectList } from "./select-list.js";
|
|
25
|
+
export { loadHistoryFile } from "./editor-history.js";
|
|
23
26
|
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
text = readFileSync(path, "utf8");
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
const entries = [];
|
|
33
|
-
for (const line of text.split("\n")) {
|
|
34
|
-
if (line.trim() === "")
|
|
35
|
-
continue;
|
|
36
|
-
try {
|
|
37
|
-
const value = JSON.parse(line);
|
|
38
|
-
if (typeof value === "string" && value !== "")
|
|
39
|
-
entries.push(value);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// 损坏的行跳过
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return entries.slice(-limit);
|
|
46
|
-
}
|
|
47
|
-
function appendHistoryFile(path, entry, limit) {
|
|
48
|
-
try {
|
|
49
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
50
|
-
appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
51
|
-
const all = loadHistoryFile(path, Number.MAX_SAFE_INTEGER);
|
|
52
|
-
if (all.length > limit * 2) {
|
|
53
|
-
writeFileSync(path, all
|
|
54
|
-
.slice(-limit)
|
|
55
|
-
.map((e) => JSON.stringify(e) + "\n")
|
|
56
|
-
.join(""));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// 历史写失败不影响编辑
|
|
61
|
-
}
|
|
62
|
-
}
|
|
27
|
+
/** 提示符 `› ` / 续行缩进的宽度。 */
|
|
28
|
+
const PROMPT_WIDTH = 2;
|
|
29
|
+
const COMPLETION_FOOTER = "Tab 接受 · Esc 关闭";
|
|
63
30
|
export class Editor {
|
|
64
31
|
options;
|
|
65
32
|
focused = false;
|
|
@@ -380,7 +347,13 @@ export class Editor {
|
|
|
380
347
|
? { value: item.value, label: item.label }
|
|
381
348
|
: { value: item.value, label: item.label, description: item.description });
|
|
382
349
|
this.completion = {
|
|
383
|
-
list: new SelectList(items, {
|
|
350
|
+
list: new SelectList(items, {
|
|
351
|
+
maxVisible: 8,
|
|
352
|
+
theme: this.theme,
|
|
353
|
+
keybindings: this.keys,
|
|
354
|
+
footer: COMPLETION_FOOTER,
|
|
355
|
+
showCount: true,
|
|
356
|
+
}),
|
|
384
357
|
from: result.from,
|
|
385
358
|
};
|
|
386
359
|
}
|
|
@@ -401,8 +374,22 @@ export class Editor {
|
|
|
401
374
|
this.completionToken++;
|
|
402
375
|
}
|
|
403
376
|
// ---- 渲染 -----------------------------------------------------------------
|
|
377
|
+
/** 文本列宽:去掉提示符与行尾光标格。 */
|
|
404
378
|
contentWidth(width) {
|
|
405
|
-
return Math.max(1, width - 1);
|
|
379
|
+
return Math.max(1, width - PROMPT_WIDTH - 1);
|
|
380
|
+
}
|
|
381
|
+
prompt() {
|
|
382
|
+
const glyph = this.theme.glyphs.prompt;
|
|
383
|
+
return this.theme.fg(this.disableSubmit ? "dim" : "user", this.theme.bold(glyph)) + " ";
|
|
384
|
+
}
|
|
385
|
+
placeholderLine(width) {
|
|
386
|
+
const text = this.options.placeholder ?? "";
|
|
387
|
+
if (!this.focused)
|
|
388
|
+
return truncateToWidth(this.prompt() + this.theme.fg("dim", text), width);
|
|
389
|
+
const first = nextGrapheme(text, 0);
|
|
390
|
+
const rest = text.slice(first.length);
|
|
391
|
+
const cursor = CURSOR_MARKER + `\x1b[7m${this.theme.fg("dim", first)}\x1b[27m`;
|
|
392
|
+
return truncateToWidth(this.prompt() + cursor + this.theme.fg("dim", rest), width);
|
|
406
393
|
}
|
|
407
394
|
/** 逻辑行 → 视觉行(按列宽折行,不切开字素与折叠标记)。 */
|
|
408
395
|
layout(width) {
|
|
@@ -494,27 +481,34 @@ export class Editor {
|
|
|
494
481
|
this.scrollTop = cursorRow - maxVisible + 1;
|
|
495
482
|
this.scrollTop = Math.max(0, Math.min(this.scrollTop, rows.length - maxVisible));
|
|
496
483
|
const end = Math.min(rows.length, this.scrollTop + maxVisible);
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
484
|
+
const g = theme.glyphs;
|
|
485
|
+
const lines = [this.border(width, this.scrollTop > 0 ? `${g.arrowUp} ${this.scrollTop}` : "")];
|
|
486
|
+
if (this.buffer.isEmpty() && this.options.placeholder) {
|
|
487
|
+
lines.push(this.placeholderLine(width));
|
|
500
488
|
}
|
|
501
489
|
else {
|
|
490
|
+
const indent = " ".repeat(PROMPT_WIDTH);
|
|
502
491
|
for (let i = this.scrollTop; i < end; i++) {
|
|
503
|
-
|
|
492
|
+
const prefix = i === 0 ? this.prompt() : indent;
|
|
493
|
+
lines.push(prefix + this.renderRow(rows[i], this.focused && i === cursorRow));
|
|
504
494
|
}
|
|
505
495
|
}
|
|
506
|
-
|
|
496
|
+
const below = rows.length - end;
|
|
497
|
+
lines.push(this.border(width, below > 0 ? `${g.arrowDown} ${below}` : ""));
|
|
507
498
|
if (this.completion) {
|
|
508
|
-
for (const line of this.completion.list.render(width))
|
|
509
|
-
lines.push(line);
|
|
499
|
+
for (const line of this.completion.list.render(Math.max(1, width - 2))) {
|
|
500
|
+
lines.push(" " + line);
|
|
501
|
+
}
|
|
510
502
|
}
|
|
511
503
|
return lines;
|
|
512
504
|
}
|
|
513
505
|
border(width, label) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
506
|
+
const rule = this.theme.glyphs.rule;
|
|
507
|
+
const labelWidth = visibleWidth(label);
|
|
508
|
+
if (label === "" || width < labelWidth + 6)
|
|
509
|
+
return this.theme.fg("border", rule.repeat(width));
|
|
510
|
+
const tail = rule.repeat(width - labelWidth - 5);
|
|
511
|
+
return (this.theme.fg("border", rule.repeat(3) + " ") +
|
|
518
512
|
this.theme.fg("dim", label) +
|
|
519
513
|
this.theme.fg("border", " " + tail));
|
|
520
514
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* - 键列宽 = 最长键的可见宽度,但不超过总宽的 `maxKeyRatio`(缺省 0.4,至少 4 列),超出的键截断;
|
|
5
5
|
* - 值在剩余宽度内截断(不换行,保留 ANSI 样式);值里的换行折成 ` ⏎ `;
|
|
6
6
|
* - 宽度连键列都放不下时退化为只显示截断后的键;空行(`key` 为空串)原样输出空行,可作分组间隔。
|
|
7
|
+
* - `wrap: true`:值按剩余宽度折行(值里的换行也换行),续行对齐值列。
|
|
7
8
|
*/
|
|
8
9
|
import type { Component, SemanticColor, Theme } from "../component.js";
|
|
9
10
|
export interface KeyValueRow {
|
|
@@ -17,6 +18,8 @@ export interface KeyValueOptions {
|
|
|
17
18
|
/** 两列之间的空白,缺省 2。 */
|
|
18
19
|
gap?: number;
|
|
19
20
|
maxKeyRatio?: number;
|
|
21
|
+
/** 值折行(续行对齐值列),缺省 false(截断)。 */
|
|
22
|
+
wrap?: boolean;
|
|
20
23
|
}
|
|
21
24
|
export declare class KeyValue implements Component {
|
|
22
25
|
private rows;
|