@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
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* - 键列宽 = 最长键的可见宽度,但不超过总宽的 `maxKeyRatio`(缺省 0.4,至少 4 列),超出的键截断;
|
|
5
5
|
* - 值在剩余宽度内截断(不换行,保留 ANSI 样式);值里的换行折成 ` ⏎ `;
|
|
6
6
|
* - 宽度连键列都放不下时退化为只显示截断后的键;空行(`key` 为空串)原样输出空行,可作分组间隔。
|
|
7
|
+
* - `wrap: true`:值按剩余宽度折行(值里的换行也换行),续行对齐值列。
|
|
7
8
|
*/
|
|
8
|
-
import { padToWidth, truncateToWidth, visibleWidth } from "../ansi.js";
|
|
9
|
+
import { padToWidth, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../ansi.js";
|
|
9
10
|
/** 键列至少保留的宽度(键更短时取键宽);再窄就只显示键。 */
|
|
10
11
|
const MIN_KEY_WIDTH = 4;
|
|
11
12
|
export class KeyValue {
|
|
@@ -29,15 +30,24 @@ export class KeyValue {
|
|
|
29
30
|
const keyWidth = Math.min(longest, Math.max(share, MIN_KEY_WIDTH));
|
|
30
31
|
const valueWidth = width - keyWidth - gap;
|
|
31
32
|
const { theme, keyColor = "dim" } = this.options;
|
|
32
|
-
const
|
|
33
|
+
const wrap = this.options.wrap === true;
|
|
34
|
+
const lines = this.rows.flatMap(({ key, value }) => {
|
|
33
35
|
if (key === "" && value === "")
|
|
34
|
-
return "";
|
|
36
|
+
return [""];
|
|
35
37
|
const shownKey = truncateToWidth(key, Math.max(keyWidth, 1));
|
|
36
38
|
const styledKey = theme ? theme.fg(keyColor, shownKey) : shownKey;
|
|
37
39
|
if (valueWidth < 1)
|
|
38
|
-
return truncateToWidth(styledKey, width);
|
|
39
|
-
const
|
|
40
|
-
|
|
40
|
+
return [truncateToWidth(styledKey, width)];
|
|
41
|
+
const head = padToWidth(styledKey, keyWidth) + " ".repeat(gap);
|
|
42
|
+
if (!wrap) {
|
|
43
|
+
const flat = value.replace(/\r?\n/g, " ⏎ ");
|
|
44
|
+
return [head + truncateToWidth(flat, valueWidth)];
|
|
45
|
+
}
|
|
46
|
+
const pieces = value
|
|
47
|
+
.split(/\r?\n/)
|
|
48
|
+
.flatMap((line) => (line === "" ? [""] : wrapTextWithAnsi(line, valueWidth)));
|
|
49
|
+
const indent = " ".repeat(keyWidth + gap);
|
|
50
|
+
return pieces.map((piece, i) => (i === 0 ? head : indent) + piece);
|
|
41
51
|
});
|
|
42
52
|
this.cache = { width, lines };
|
|
43
53
|
return lines;
|
|
@@ -1,35 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 运行指示(设计 §12.1、§12.6;终端界面视觉设计 v1 §3.7)。[B4]
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 一行 `⠋ 动词 · 已用时 · 附加项…`:spinner `accent`、动词正文色、其余 `dim`,以 ` · ` 连接。
|
|
5
|
+
* - `setVerb(verb, extras, { elapsed })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
|
|
6
|
+
* `setMessage(text)` 是只换动词的旧接口。
|
|
7
|
+
* - 帧取 `theme.glyphs.spinner`(Unicode 10 帧 80 ms、ASCII 4 帧 250 ms);`frame` 暴露当前帧字形,
|
|
8
|
+
* `onFrame` 让别的组件(运行中的工具摘要行)与 Loader 同帧换字,保证一帧只多改一行。
|
|
9
|
+
* - `animation: false`:spinner 固定为 `glyphs.spinnerStatic`,定时器每秒一次,已用时变了才请求重绘。
|
|
10
|
+
* - `stop()` 后不再触发渲染。时钟可注入便于测试。
|
|
5
11
|
*/
|
|
6
12
|
import type { Component, Theme } from "../component.js";
|
|
7
|
-
|
|
13
|
+
/** Unicode 帧表(兼容旧导出;实际帧取 `theme.glyphs.spinner`)。 */
|
|
14
|
+
export declare const LOADER_FRAMES: readonly string[];
|
|
8
15
|
export interface LoaderOptions {
|
|
16
|
+
/** 初始动词(旧名 message)。 */
|
|
9
17
|
message?: string;
|
|
10
18
|
theme?: Theme;
|
|
11
|
-
/** 帧间隔(ms),缺省 80。 */
|
|
19
|
+
/** 帧间隔(ms),缺省 Unicode 80、ASCII 250;`animation: false` 时为 1000。 */
|
|
12
20
|
intervalMs?: number;
|
|
13
21
|
/** 显示已用时(`12s`、`1m05s`),缺省 true。 */
|
|
14
22
|
showElapsed?: boolean;
|
|
23
|
+
/** false:spinner 静止,缺省 true。 */
|
|
24
|
+
animation?: boolean;
|
|
15
25
|
now?: () => number;
|
|
16
26
|
}
|
|
27
|
+
export interface LoaderVerbOptions {
|
|
28
|
+
/** 本动词是否显示已用时(缺省跟随 `showElapsed`)。 */
|
|
29
|
+
elapsed?: boolean;
|
|
30
|
+
}
|
|
17
31
|
export declare function formatElapsed(ms: number): string;
|
|
18
32
|
export declare class Loader implements Component {
|
|
19
33
|
private readonly requestRender;
|
|
20
34
|
private readonly options;
|
|
21
|
-
private
|
|
35
|
+
private frameIndex;
|
|
22
36
|
private timer;
|
|
23
37
|
private startedAt;
|
|
24
|
-
private
|
|
38
|
+
private verb;
|
|
39
|
+
private extras;
|
|
40
|
+
private verbElapsed;
|
|
41
|
+
private lastElapsed;
|
|
42
|
+
private readonly listeners;
|
|
25
43
|
private readonly now;
|
|
26
44
|
constructor(requestRender: () => void, options?: LoaderOptions);
|
|
27
45
|
get running(): boolean;
|
|
46
|
+
private get frames();
|
|
47
|
+
private get animated();
|
|
48
|
+
/** 当前帧字形(静止时为 `spinnerStatic`)。 */
|
|
49
|
+
get frame(): string;
|
|
50
|
+
/** 订阅换帧(返回取消函数)。 */
|
|
51
|
+
onFrame(listener: (frame: string) => void): () => void;
|
|
52
|
+
/** 换动词与附加项(附加项 dim,排在已用时之后)。 */
|
|
53
|
+
setVerb(verb: string, extras?: readonly string[], options?: LoaderVerbOptions): void;
|
|
54
|
+
/** 旧接口:只换动词。 */
|
|
28
55
|
setMessage(message: string): void;
|
|
56
|
+
/** 已用时从现在重新算(换到另一段工作时)。 */
|
|
57
|
+
resetElapsed(): void;
|
|
29
58
|
start(): void;
|
|
30
59
|
stop(): void;
|
|
31
|
-
/**
|
|
60
|
+
/** 推进一帧(定时器调用;测试可直接调用)。静止时只在已用时变化时重绘。 */
|
|
32
61
|
tick(): void;
|
|
62
|
+
private elapsedText;
|
|
33
63
|
render(width: number): string[];
|
|
34
64
|
invalidate(): void;
|
|
35
65
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 运行指示(设计 §12.1、§12.6;终端界面视觉设计 v1 §3.7)。[B4]
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 一行 `⠋ 动词 · 已用时 · 附加项…`:spinner `accent`、动词正文色、其余 `dim`,以 ` · ` 连接。
|
|
5
|
+
* - `setVerb(verb, extras, { elapsed })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
|
|
6
|
+
* `setMessage(text)` 是只换动词的旧接口。
|
|
7
|
+
* - 帧取 `theme.glyphs.spinner`(Unicode 10 帧 80 ms、ASCII 4 帧 250 ms);`frame` 暴露当前帧字形,
|
|
8
|
+
* `onFrame` 让别的组件(运行中的工具摘要行)与 Loader 同帧换字,保证一帧只多改一行。
|
|
9
|
+
* - `animation: false`:spinner 固定为 `glyphs.spinnerStatic`,定时器每秒一次,已用时变了才请求重绘。
|
|
10
|
+
* - `stop()` 后不再触发渲染。时钟可注入便于测试。
|
|
5
11
|
*/
|
|
6
12
|
import { truncateToWidth } from "../ansi.js";
|
|
7
|
-
|
|
13
|
+
import { UNICODE_GLYPHS } from "../glyphs.js";
|
|
14
|
+
/** Unicode 帧表(兼容旧导出;实际帧取 `theme.glyphs.spinner`)。 */
|
|
15
|
+
export const LOADER_FRAMES = UNICODE_GLYPHS.spinner;
|
|
8
16
|
export function formatElapsed(ms) {
|
|
9
17
|
const total = Math.max(0, Math.floor(ms / 1000));
|
|
10
18
|
if (total < 60)
|
|
@@ -16,31 +24,74 @@ export function formatElapsed(ms) {
|
|
|
16
24
|
export class Loader {
|
|
17
25
|
requestRender;
|
|
18
26
|
options;
|
|
19
|
-
|
|
27
|
+
frameIndex = 0;
|
|
20
28
|
timer = null;
|
|
21
29
|
startedAt;
|
|
22
|
-
|
|
30
|
+
verb;
|
|
31
|
+
extras = [];
|
|
32
|
+
verbElapsed;
|
|
33
|
+
lastElapsed = "";
|
|
34
|
+
listeners = new Set();
|
|
23
35
|
now;
|
|
24
36
|
constructor(requestRender, options = {}) {
|
|
25
37
|
this.requestRender = requestRender;
|
|
26
38
|
this.options = options;
|
|
27
39
|
this.now = options.now ?? Date.now;
|
|
28
|
-
this.
|
|
40
|
+
this.verb = options.message ?? "";
|
|
29
41
|
this.startedAt = this.now();
|
|
30
42
|
}
|
|
31
43
|
get running() {
|
|
32
44
|
return this.timer !== null;
|
|
33
45
|
}
|
|
34
|
-
|
|
35
|
-
this.
|
|
46
|
+
get frames() {
|
|
47
|
+
return (this.options.theme?.glyphs ?? UNICODE_GLYPHS).spinner;
|
|
48
|
+
}
|
|
49
|
+
get animated() {
|
|
50
|
+
return this.options.animation !== false;
|
|
51
|
+
}
|
|
52
|
+
/** 当前帧字形(静止时为 `spinnerStatic`)。 */
|
|
53
|
+
get frame() {
|
|
54
|
+
const glyphs = this.options.theme?.glyphs ?? UNICODE_GLYPHS;
|
|
55
|
+
return this.animated
|
|
56
|
+
? this.frames[this.frameIndex % this.frames.length]
|
|
57
|
+
: glyphs.spinnerStatic;
|
|
58
|
+
}
|
|
59
|
+
/** 订阅换帧(返回取消函数)。 */
|
|
60
|
+
onFrame(listener) {
|
|
61
|
+
this.listeners.add(listener);
|
|
62
|
+
return () => this.listeners.delete(listener);
|
|
63
|
+
}
|
|
64
|
+
/** 换动词与附加项(附加项 dim,排在已用时之后)。 */
|
|
65
|
+
setVerb(verb, extras = [], options = {}) {
|
|
66
|
+
const same = verb === this.verb &&
|
|
67
|
+
options.elapsed === this.verbElapsed &&
|
|
68
|
+
extras.length === this.extras.length &&
|
|
69
|
+
extras.every((e, i) => e === this.extras[i]);
|
|
70
|
+
if (same)
|
|
71
|
+
return;
|
|
72
|
+
this.verb = verb;
|
|
73
|
+
this.extras = [...extras];
|
|
74
|
+
this.verbElapsed = options.elapsed;
|
|
36
75
|
this.requestRender();
|
|
37
76
|
}
|
|
77
|
+
/** 旧接口:只换动词。 */
|
|
78
|
+
setMessage(message) {
|
|
79
|
+
this.setVerb(message);
|
|
80
|
+
}
|
|
81
|
+
/** 已用时从现在重新算(换到另一段工作时)。 */
|
|
82
|
+
resetElapsed() {
|
|
83
|
+
this.startedAt = this.now();
|
|
84
|
+
}
|
|
38
85
|
start() {
|
|
39
86
|
if (this.timer !== null)
|
|
40
87
|
return;
|
|
41
88
|
this.startedAt = this.now();
|
|
42
|
-
this.
|
|
43
|
-
|
|
89
|
+
this.frameIndex = 0;
|
|
90
|
+
const glyphs = this.options.theme?.glyphs ?? UNICODE_GLYPHS;
|
|
91
|
+
const interval = this.animated
|
|
92
|
+
? (this.options.intervalMs ?? (glyphs.ascii ? 250 : 80))
|
|
93
|
+
: Math.max(this.options.intervalMs ?? 1000, 1000);
|
|
94
|
+
this.timer = setInterval(() => this.tick(), interval);
|
|
44
95
|
this.timer.unref?.();
|
|
45
96
|
this.requestRender();
|
|
46
97
|
}
|
|
@@ -50,22 +101,34 @@ export class Loader {
|
|
|
50
101
|
clearInterval(this.timer);
|
|
51
102
|
this.timer = null;
|
|
52
103
|
}
|
|
53
|
-
/**
|
|
104
|
+
/** 推进一帧(定时器调用;测试可直接调用)。静止时只在已用时变化时重绘。 */
|
|
54
105
|
tick() {
|
|
55
|
-
|
|
106
|
+
if (this.animated)
|
|
107
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
108
|
+
else if (this.elapsedText() === this.lastElapsed)
|
|
109
|
+
return;
|
|
110
|
+
const frame = this.frame;
|
|
111
|
+
for (const listener of this.listeners)
|
|
112
|
+
listener(frame);
|
|
56
113
|
this.requestRender();
|
|
57
114
|
}
|
|
115
|
+
elapsedText() {
|
|
116
|
+
return formatElapsed(this.now() - this.startedAt);
|
|
117
|
+
}
|
|
58
118
|
render(width) {
|
|
59
119
|
const theme = this.options.theme;
|
|
60
|
-
const
|
|
61
|
-
const parts = [
|
|
62
|
-
if (this.
|
|
63
|
-
parts.push(this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
120
|
+
const dim = (s) => (theme ? theme.fg("dim", s) : s);
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (this.verb !== "")
|
|
123
|
+
parts.push(this.verb);
|
|
124
|
+
const elapsed = this.elapsedText();
|
|
125
|
+
this.lastElapsed = elapsed;
|
|
126
|
+
if (this.verbElapsed ?? this.options.showElapsed !== false)
|
|
127
|
+
parts.push(dim(elapsed));
|
|
128
|
+
for (const extra of this.extras)
|
|
129
|
+
parts.push(dim(extra));
|
|
130
|
+
const spinner = theme ? theme.fg("accent", this.frame) : this.frame;
|
|
131
|
+
return [truncateToWidth(`${spinner} ${parts.join(dim(" · "))}`, width)];
|
|
69
132
|
}
|
|
70
133
|
invalidate() { }
|
|
71
134
|
}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* 支持:ATX 标题、段落(软换行合并为空格,行尾两个空格或 `\` 为硬换行)、无序 / 有序列表(缩进嵌套)、
|
|
5
5
|
* 围栏代码块(边框 + 语言标签;流式时未闭合的围栏按代码块渲染)、引用、分隔线、
|
|
6
6
|
* 行内 `code` / **粗体** / *斜体* / ~~删除线~~ / [链接](url) / <自动链接> / 反斜杠转义。
|
|
7
|
-
*
|
|
7
|
+
* 表格降级为等宽对齐文本(列间两个空格、首行粗体其下一条规则线、不换行、超宽截断);不做语法高亮(§12.9)。
|
|
8
|
+
*
|
|
9
|
+
* 配色(终端界面视觉设计 v1 §3.3):h1 / h2 `accent` 粗体,h3+ 粗体;行内代码 `code`;链接文字 `link`
|
|
10
|
+
* 加下划线、URL 以 ` (url)` `dim` 追加;列表符号 `muted`;代码块全宽、边框 `border`、语言标签 `dim`、
|
|
11
|
+
* 正文 `text`;引用 `▎` + `muted` 斜体。字形(圆点、框线、竖条)取 `theme.glyphs`。
|
|
8
12
|
*
|
|
9
13
|
* 缓存:按块缓存渲染结果(键 = 宽度 + 块源文本),流式追加时只有末块重新渲染;解析也是增量的
|
|
10
14
|
* (`Markdown.append` 只从尾部可能变化的块重新解析)。
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* 支持:ATX 标题、段落(软换行合并为空格,行尾两个空格或 `\` 为硬换行)、无序 / 有序列表(缩进嵌套)、
|
|
5
5
|
* 围栏代码块(边框 + 语言标签;流式时未闭合的围栏按代码块渲染)、引用、分隔线、
|
|
6
6
|
* 行内 `code` / **粗体** / *斜体* / ~~删除线~~ / [链接](url) / <自动链接> / 反斜杠转义。
|
|
7
|
-
*
|
|
7
|
+
* 表格降级为等宽对齐文本(列间两个空格、首行粗体其下一条规则线、不换行、超宽截断);不做语法高亮(§12.9)。
|
|
8
|
+
*
|
|
9
|
+
* 配色(终端界面视觉设计 v1 §3.3):h1 / h2 `accent` 粗体,h3+ 粗体;行内代码 `code`;链接文字 `link`
|
|
10
|
+
* 加下划线、URL 以 ` (url)` `dim` 追加;列表符号 `muted`;代码块全宽、边框 `border`、语言标签 `dim`、
|
|
11
|
+
* 正文 `text`;引用 `▎` + `muted` 斜体。字形(圆点、框线、竖条)取 `theme.glyphs`。
|
|
8
12
|
*
|
|
9
13
|
* 缓存:按块缓存渲染结果(键 = 宽度 + 块源文本),流式追加时只有末块重新渲染;解析也是增量的
|
|
10
14
|
* (`Markdown.append` 只从尾部可能变化的块重新解析)。
|
|
@@ -208,8 +212,8 @@ export function renderInline(text, theme) {
|
|
|
208
212
|
const url = link[2];
|
|
209
213
|
out +=
|
|
210
214
|
link[1] === url || url === ""
|
|
211
|
-
? theme.underline(label || url)
|
|
212
|
-
: theme.underline(label) + theme.fg("dim", ` (${url})`);
|
|
215
|
+
? theme.fg("link", theme.underline(label || url))
|
|
216
|
+
: theme.fg("link", theme.underline(label)) + theme.fg("dim", ` (${url})`);
|
|
213
217
|
i += link[0].length;
|
|
214
218
|
continue;
|
|
215
219
|
}
|
|
@@ -217,7 +221,7 @@ export function renderInline(text, theme) {
|
|
|
217
221
|
if (ch === "<") {
|
|
218
222
|
const auto = /^<((?:https?|mailto):[^>\s]+)>/.exec(text.slice(i));
|
|
219
223
|
if (auto) {
|
|
220
|
-
out += theme.underline(auto[1]);
|
|
224
|
+
out += theme.fg("link", theme.underline(auto[1]));
|
|
221
225
|
i += auto[0].length;
|
|
222
226
|
continue;
|
|
223
227
|
}
|
|
@@ -263,17 +267,19 @@ export function renderBlock(block, width, theme) {
|
|
|
263
267
|
switch (block.kind) {
|
|
264
268
|
case "heading": {
|
|
265
269
|
const text = renderInline(block.text, theme);
|
|
266
|
-
const styled = block.level <= 2
|
|
270
|
+
const styled = block.level <= 2
|
|
271
|
+
? theme.fg("accent", theme.bold(text))
|
|
272
|
+
: theme.fg("text", theme.bold(text));
|
|
267
273
|
return wrapTextWithAnsi(styled, width);
|
|
268
274
|
}
|
|
269
275
|
case "paragraph":
|
|
270
276
|
return wrapTextWithAnsi(renderInline(block.text, theme), width);
|
|
271
277
|
case "hr":
|
|
272
|
-
return [theme.fg("border",
|
|
278
|
+
return [theme.fg("border", theme.glyphs.rule.repeat(Math.max(1, width)))];
|
|
273
279
|
case "quote": {
|
|
274
|
-
const bar = theme.fg("border", "
|
|
275
|
-
const
|
|
276
|
-
return
|
|
280
|
+
const bar = theme.fg("border", theme.glyphs.card) + " ";
|
|
281
|
+
const body = theme.fg("muted", theme.italic(renderInline(block.text, theme)));
|
|
282
|
+
return wrapTextWithAnsi(body, Math.max(1, width - 2)).map((line) => bar + line);
|
|
277
283
|
}
|
|
278
284
|
case "list":
|
|
279
285
|
return renderList(block.items, width, theme);
|
|
@@ -283,15 +289,24 @@ export function renderBlock(block, width, theme) {
|
|
|
283
289
|
return renderTable(block.rows, width, theme);
|
|
284
290
|
}
|
|
285
291
|
}
|
|
286
|
-
const BULLETS = ["•", "◦", "▪"];
|
|
287
292
|
function renderList(items, width, theme) {
|
|
288
293
|
const lines = [];
|
|
294
|
+
const bullets = theme.glyphs.bullets;
|
|
295
|
+
/** 各层的正文起始列与无序层数(嵌套项对齐父项正文,圆点按无序层数换)。 */
|
|
296
|
+
const columns = [];
|
|
297
|
+
const unordered = [];
|
|
289
298
|
for (const item of items) {
|
|
290
|
-
const
|
|
299
|
+
const depth = Math.min(item.depth, columns.length);
|
|
300
|
+
const indent = depth === 0 ? 0 : columns[depth - 1];
|
|
291
301
|
const ordered = /\d/.test(item.marker);
|
|
292
|
-
const
|
|
293
|
-
const
|
|
302
|
+
const level = (depth === 0 ? 0 : unordered[depth - 1]) + (ordered ? 0 : 1);
|
|
303
|
+
const bullet = ordered ? item.marker : bullets[(level - 1) % bullets.length];
|
|
304
|
+
const head = " ".repeat(indent) + theme.fg("muted", bullet) + " ";
|
|
294
305
|
const headWidth = visibleWidth(head);
|
|
306
|
+
columns.length = depth;
|
|
307
|
+
unordered.length = depth;
|
|
308
|
+
columns.push(headWidth);
|
|
309
|
+
unordered.push(level);
|
|
295
310
|
const body = wrapTextWithAnsi(renderInline(item.text, theme), Math.max(1, width - headWidth));
|
|
296
311
|
body.forEach((line, index) => {
|
|
297
312
|
lines.push((index === 0 ? head : " ".repeat(headWidth)) + line);
|
|
@@ -304,18 +319,21 @@ function renderCode(lang, body, width, theme) {
|
|
|
304
319
|
if (width < 8)
|
|
305
320
|
return code.map((line) => truncateToWidth(line, width));
|
|
306
321
|
const inner = width - 4;
|
|
322
|
+
const g = theme.glyphs.box;
|
|
307
323
|
const border = (s) => theme.fg("border", s);
|
|
308
324
|
const label = lang === "" ? "" : truncateToWidth(` ${lang} `, inner - 1);
|
|
309
|
-
const top = border(
|
|
325
|
+
const top = border(g.topLeft + g.horizontal) +
|
|
310
326
|
theme.fg("dim", label) +
|
|
311
|
-
border(
|
|
327
|
+
border(g.horizontal.repeat(width - 3 - visibleWidth(label)) + g.topRight);
|
|
312
328
|
const lines = [top];
|
|
313
329
|
for (const line of code) {
|
|
314
330
|
for (const piece of wrapCode(line, inner)) {
|
|
315
|
-
lines.push(border(
|
|
331
|
+
lines.push(border(g.vertical + " ") +
|
|
332
|
+
padToWidth(theme.fg("text", piece), inner) +
|
|
333
|
+
border(" " + g.vertical));
|
|
316
334
|
}
|
|
317
335
|
}
|
|
318
|
-
lines.push(border(
|
|
336
|
+
lines.push(border(g.bottomLeft + g.horizontal.repeat(width - 2) + g.bottomRight));
|
|
319
337
|
return lines;
|
|
320
338
|
}
|
|
321
339
|
/** 代码按列硬断行(不按词),空行保留。 */
|
|
@@ -345,12 +363,18 @@ function renderTable(rows, width, theme) {
|
|
|
345
363
|
for (const row of rendered) {
|
|
346
364
|
row.forEach((cell, c) => (widths[c] = Math.max(widths[c], visibleWidth(cell))));
|
|
347
365
|
}
|
|
348
|
-
const
|
|
349
|
-
|
|
366
|
+
const lines = [];
|
|
367
|
+
rendered.forEach((row, r) => {
|
|
350
368
|
const cells = widths.map((w, c) => padToWidth(row[c] ?? "", w));
|
|
351
|
-
const line = cells.join(
|
|
352
|
-
|
|
369
|
+
const line = cells.join(" ").replace(/\s+$/, "");
|
|
370
|
+
const header = r === 0 && rows.length > 1;
|
|
371
|
+
lines.push(truncateToWidth(header ? theme.bold(line) : line, width));
|
|
372
|
+
if (header) {
|
|
373
|
+
const total = widths.reduce((sum, w) => sum + w, 0) + 2 * Math.max(0, cols - 1);
|
|
374
|
+
lines.push(theme.fg("border", theme.glyphs.rule.repeat(Math.max(1, Math.min(total, width)))));
|
|
375
|
+
}
|
|
353
376
|
});
|
|
377
|
+
return lines;
|
|
354
378
|
}
|
|
355
379
|
export class Markdown {
|
|
356
380
|
text;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - `value` 是 0–1 的比例(越界夹紧);undefined 显示 `—`,格子全空;
|
|
5
5
|
* - 阈值着色(需要 theme):≥ `dangerAt`(缺省 0.9)error,≥ `warnAt`(缺省 0.7)warning,否则 success;
|
|
6
6
|
* 空格子 dim;
|
|
7
|
-
* - 宽度不够时先去掉格子,只剩「标签
|
|
7
|
+
* - 宽度不够时先去掉格子,只剩「标签 百分比」,再不够就截断;格子字形取 `theme.glyphs`(ASCII `# .`)。
|
|
8
8
|
*/
|
|
9
9
|
import type { Component, SemanticColor, Theme } from "../component.js";
|
|
10
10
|
export interface MeterOptions {
|
|
@@ -15,8 +15,8 @@ export interface MeterOptions {
|
|
|
15
15
|
dangerAt?: number;
|
|
16
16
|
theme?: Theme;
|
|
17
17
|
}
|
|
18
|
-
export declare const METER_FULL
|
|
19
|
-
export declare const METER_EMPTY
|
|
18
|
+
export declare const METER_FULL: string;
|
|
19
|
+
export declare const METER_EMPTY: string;
|
|
20
20
|
export declare class Meter implements Component {
|
|
21
21
|
private value;
|
|
22
22
|
private readonly options;
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* - `value` 是 0–1 的比例(越界夹紧);undefined 显示 `—`,格子全空;
|
|
5
5
|
* - 阈值着色(需要 theme):≥ `dangerAt`(缺省 0.9)error,≥ `warnAt`(缺省 0.7)warning,否则 success;
|
|
6
6
|
* 空格子 dim;
|
|
7
|
-
* - 宽度不够时先去掉格子,只剩「标签
|
|
7
|
+
* - 宽度不够时先去掉格子,只剩「标签 百分比」,再不够就截断;格子字形取 `theme.glyphs`(ASCII `# .`)。
|
|
8
8
|
*/
|
|
9
9
|
import { truncateToWidth, visibleWidth } from "../ansi.js";
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { levelColor } from "../theme.js";
|
|
11
|
+
import { UNICODE_GLYPHS } from "../glyphs.js";
|
|
12
|
+
export const METER_FULL = UNICODE_GLYPHS.meterFull;
|
|
13
|
+
export const METER_EMPTY = UNICODE_GLYPHS.meterEmpty;
|
|
12
14
|
export class Meter {
|
|
13
15
|
value;
|
|
14
16
|
options;
|
|
@@ -21,12 +23,11 @@ export class Meter {
|
|
|
21
23
|
}
|
|
22
24
|
/** 当前阈值档位对应的语义色。 */
|
|
23
25
|
level() {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return "success";
|
|
26
|
+
const { warnAt, dangerAt } = this.options;
|
|
27
|
+
return levelColor(this.value ?? 0, {
|
|
28
|
+
...(warnAt !== undefined ? { warnAt } : {}),
|
|
29
|
+
...(dangerAt !== undefined ? { dangerAt } : {}),
|
|
30
|
+
});
|
|
30
31
|
}
|
|
31
32
|
render(width) {
|
|
32
33
|
const { theme, label, cells = 10 } = this.options;
|
|
@@ -35,8 +36,9 @@ export class Meter {
|
|
|
35
36
|
const color = (c, text) => (theme ? theme.fg(c, text) : text);
|
|
36
37
|
const head = label === undefined || label === "" ? "" : `${label} `;
|
|
37
38
|
const filled = ratio === undefined ? 0 : Math.round(ratio * cells);
|
|
38
|
-
const
|
|
39
|
-
|
|
39
|
+
const glyphs = theme?.glyphs ?? UNICODE_GLYPHS;
|
|
40
|
+
const bar = color(this.level(), glyphs.meterFull.repeat(filled)) +
|
|
41
|
+
color("dim", glyphs.meterEmpty.repeat(cells - filled));
|
|
40
42
|
const tail = color(ratio === undefined ? "dim" : this.level(), percent);
|
|
41
43
|
const full = `${head}${bar} ${tail}`;
|
|
42
44
|
if (visibleWidth(full) <= width)
|
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* 键位走 `tui.select.*`:上下移动(循环)、翻页、确认(Enter / Tab)、取消(Esc / Ctrl+C)。
|
|
5
5
|
* `filterable` 时可打印字符追加到过滤词、退格删除;过滤为大小写不敏感的分词子串匹配
|
|
6
6
|
* (每个词都要出现在 label / value / group / description 之一中)。
|
|
7
|
+
*
|
|
8
|
+
* `badge` 靠右显示(如 `Default`、`Recommended`);`numberKeys` 时右侧标 1–9,按数字直接选中并确认;
|
|
9
|
+
* `stacked` 时说明换到标签下一行(缩进、暗色),适合「名字 + 一行说明」的短列表。
|
|
10
|
+
*
|
|
11
|
+
* 终端界面视觉设计 v1:选中行 `›` + `selection` 底色(< 256 色退化为 accent 粗体,stacked 时两行都上);
|
|
12
|
+
* `currentValue` 的项标 `✓`;`footer` 是列表下的一行按键提示(dim),`showCount` 时前面带 `(i/n)`
|
|
13
|
+
* (缺省只在超出一屏时带);过滤框提示符 `›`。字形取 `theme.glyphs`。
|
|
7
14
|
*/
|
|
8
|
-
import type { Component, Focusable, Theme } from "../component.js";
|
|
15
|
+
import type { Component, Focusable, SemanticColor, Theme } from "../component.js";
|
|
9
16
|
import { type Keybindings } from "../keybindings.js";
|
|
10
17
|
export interface SelectItem {
|
|
11
18
|
value: string;
|
|
@@ -13,6 +20,10 @@ export interface SelectItem {
|
|
|
13
20
|
description?: string;
|
|
14
21
|
/** 相邻项 group 不同时插入分组标题行。 */
|
|
15
22
|
group?: string;
|
|
23
|
+
/** 靠右的徽标文字。 */
|
|
24
|
+
badge?: string;
|
|
25
|
+
/** 徽标颜色,缺省 accent。 */
|
|
26
|
+
badgeColor?: SemanticColor;
|
|
16
27
|
}
|
|
17
28
|
export interface SelectListOptions {
|
|
18
29
|
/** 一屏最多显示的条目数,缺省 10。 */
|
|
@@ -21,6 +32,16 @@ export interface SelectListOptions {
|
|
|
21
32
|
keybindings?: Keybindings;
|
|
22
33
|
filterable?: boolean;
|
|
23
34
|
emptyText?: string;
|
|
35
|
+
/** 右侧标序号,按 1–9 直接选中并确认(不与 filterable 同用)。 */
|
|
36
|
+
numberKeys?: boolean;
|
|
37
|
+
/** 说明放在标签下一行。 */
|
|
38
|
+
stacked?: boolean;
|
|
39
|
+
/** 列表下的按键提示行(dim)。 */
|
|
40
|
+
footer?: string;
|
|
41
|
+
/** 提示行前带 `(i/n)`;缺省只在超出一屏时带。 */
|
|
42
|
+
showCount?: boolean;
|
|
43
|
+
/** 当前值:该项标签前标 `✓`(其余项留同宽空白)。 */
|
|
44
|
+
currentValue?: string;
|
|
24
45
|
onSelect?(item: SelectItem): void;
|
|
25
46
|
onCancel?(): void;
|
|
26
47
|
onSelectionChange?(item: SelectItem | undefined): void;
|
|
@@ -48,6 +69,7 @@ export declare class SelectList implements Component, Focusable {
|
|
|
48
69
|
selectValue(value: string): boolean;
|
|
49
70
|
moveSelection(delta: number, wrap?: boolean): void;
|
|
50
71
|
handleInput(data: string): void;
|
|
72
|
+
private get glyphs();
|
|
51
73
|
render(width: number): string[];
|
|
52
74
|
invalidate(): void;
|
|
53
75
|
private renderItem;
|