skyloom 1.13.5 → 1.13.7
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/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/guard.d.ts +45 -0
- package/dist/core/agent/guard.d.ts.map +1 -0
- package/dist/core/agent/guard.js +113 -0
- package/dist/core/agent/guard.js.map +1 -0
- package/dist/core/agent.d.ts +17 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +182 -93
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +1 -1
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -0
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +177 -95
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -0
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for tool subset routing.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from 'vitest';
|
|
5
|
-
import { selectRelevantTools } from '../src/core/tool_router';
|
|
6
|
-
import { ToolRegistry } from '../src/core/tool';
|
|
7
|
-
|
|
8
|
-
function makeRegistry(toolSpecs: Array<[string, string]>): ToolRegistry {
|
|
9
|
-
const r = new ToolRegistry();
|
|
10
|
-
for (const [name, desc] of toolSpecs) {
|
|
11
|
-
r.register({
|
|
12
|
-
name,
|
|
13
|
-
description: desc,
|
|
14
|
-
parameters: [{ name: 'x', type: 'string', description: 'x' }],
|
|
15
|
-
handler: async () => 'ok',
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
return r;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('selectRelevantTools', () => {
|
|
22
|
-
it('short query returns full set', () => {
|
|
23
|
-
const r = makeRegistry(Array.from({ length: 20 }, (_, i) => [`tool_${i}`, `desc ${i}`]));
|
|
24
|
-
const names = r.listNames();
|
|
25
|
-
const selected = selectRelevantTools(r, names, 'ok', { topK: 5 });
|
|
26
|
-
expect(new Set(selected)).toEqual(new Set(names));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('small catalog returns full set', () => {
|
|
30
|
-
const r = makeRegistry([['a', 'alpha'], ['b', 'beta']]);
|
|
31
|
-
const names = r.listNames();
|
|
32
|
-
const selected = selectRelevantTools(r, names, 'anything goes here', { topK: 12 });
|
|
33
|
-
expect(new Set(selected)).toEqual(new Set(names));
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('caps at topK for large catalog', () => {
|
|
37
|
-
const r = makeRegistry(Array.from({ length: 50 }, (_, i) => [`tool_${i}`, `desc ${i}`]));
|
|
38
|
-
const names = r.listNames();
|
|
39
|
-
const selected = selectRelevantTools(r, names, 'find weather data', { topK: 10 });
|
|
40
|
-
expect(selected.length).toBeLessThanOrEqual(10);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('relevant tools score higher', () => {
|
|
44
|
-
const r = makeRegistry([
|
|
45
|
-
['read_file', 'read a file from disk'],
|
|
46
|
-
['write_file', 'write content to a file'],
|
|
47
|
-
['send_email', 'send an email message'],
|
|
48
|
-
['fetch_url', 'fetch a web URL'],
|
|
49
|
-
['query_db', 'query the database'],
|
|
50
|
-
]);
|
|
51
|
-
const names = r.listNames();
|
|
52
|
-
const selected = selectRelevantTools(r, names, 'read this config file', { topK: 3 });
|
|
53
|
-
expect(selected).toContain('read_file');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('mustInclude always present', () => {
|
|
57
|
-
const r = makeRegistry(Array.from({ length: 20 }, (_, i) => [`random_${i}`, `unrelated tool ${i}`]));
|
|
58
|
-
r.register({
|
|
59
|
-
name: 'critical_tool',
|
|
60
|
-
description: 'must always be available',
|
|
61
|
-
parameters: [{ name: 'x', type: 'string', description: 'x' }],
|
|
62
|
-
handler: async () => 'ok',
|
|
63
|
-
});
|
|
64
|
-
const names = r.listNames();
|
|
65
|
-
const selected = selectRelevantTools(r, names, 'totally unrelated query', {
|
|
66
|
-
topK: 3,
|
|
67
|
-
mustInclude: new Set(['critical_tool']),
|
|
68
|
-
});
|
|
69
|
-
expect(selected).toContain('critical_tool');
|
|
70
|
-
});
|
|
71
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tool subset routing.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { selectRelevantTools } from '../src/core/tool_router';
|
|
6
|
+
import { ToolRegistry } from '../src/core/tool';
|
|
7
|
+
|
|
8
|
+
function makeRegistry(toolSpecs: Array<[string, string]>): ToolRegistry {
|
|
9
|
+
const r = new ToolRegistry();
|
|
10
|
+
for (const [name, desc] of toolSpecs) {
|
|
11
|
+
r.register({
|
|
12
|
+
name,
|
|
13
|
+
description: desc,
|
|
14
|
+
parameters: [{ name: 'x', type: 'string', description: 'x' }],
|
|
15
|
+
handler: async () => 'ok',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return r;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('selectRelevantTools', () => {
|
|
22
|
+
it('short query returns full set', () => {
|
|
23
|
+
const r = makeRegistry(Array.from({ length: 20 }, (_, i) => [`tool_${i}`, `desc ${i}`]));
|
|
24
|
+
const names = r.listNames();
|
|
25
|
+
const selected = selectRelevantTools(r, names, 'ok', { topK: 5 });
|
|
26
|
+
expect(new Set(selected)).toEqual(new Set(names));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('small catalog returns full set', () => {
|
|
30
|
+
const r = makeRegistry([['a', 'alpha'], ['b', 'beta']]);
|
|
31
|
+
const names = r.listNames();
|
|
32
|
+
const selected = selectRelevantTools(r, names, 'anything goes here', { topK: 12 });
|
|
33
|
+
expect(new Set(selected)).toEqual(new Set(names));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('caps at topK for large catalog', () => {
|
|
37
|
+
const r = makeRegistry(Array.from({ length: 50 }, (_, i) => [`tool_${i}`, `desc ${i}`]));
|
|
38
|
+
const names = r.listNames();
|
|
39
|
+
const selected = selectRelevantTools(r, names, 'find weather data', { topK: 10 });
|
|
40
|
+
expect(selected.length).toBeLessThanOrEqual(10);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('relevant tools score higher', () => {
|
|
44
|
+
const r = makeRegistry([
|
|
45
|
+
['read_file', 'read a file from disk'],
|
|
46
|
+
['write_file', 'write content to a file'],
|
|
47
|
+
['send_email', 'send an email message'],
|
|
48
|
+
['fetch_url', 'fetch a web URL'],
|
|
49
|
+
['query_db', 'query the database'],
|
|
50
|
+
]);
|
|
51
|
+
const names = r.listNames();
|
|
52
|
+
const selected = selectRelevantTools(r, names, 'read this config file', { topK: 3 });
|
|
53
|
+
expect(selected).toContain('read_file');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('mustInclude always present', () => {
|
|
57
|
+
const r = makeRegistry(Array.from({ length: 20 }, (_, i) => [`random_${i}`, `unrelated tool ${i}`]));
|
|
58
|
+
r.register({
|
|
59
|
+
name: 'critical_tool',
|
|
60
|
+
description: 'must always be available',
|
|
61
|
+
parameters: [{ name: 'x', type: 'string', description: 'x' }],
|
|
62
|
+
handler: async () => 'ok',
|
|
63
|
+
});
|
|
64
|
+
const names = r.listNames();
|
|
65
|
+
const selected = selectRelevantTools(r, names, 'totally unrelated query', {
|
|
66
|
+
topK: 3,
|
|
67
|
+
mustInclude: new Set(['critical_tool']),
|
|
68
|
+
});
|
|
69
|
+
expect(selected).toContain('critical_tool');
|
|
70
|
+
});
|
|
71
|
+
});
|
package/tests/tui.test.ts
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { charWidth, visualWidth, padVisual, StreamRenderer } from "../src/cli/tui";
|
|
3
|
-
|
|
4
|
-
describe("CJK-aware width", () => {
|
|
5
|
-
it("counts ascii as 1, CJK as 2", () => {
|
|
6
|
-
expect(charWidth("a".codePointAt(0)!)).toBe(1);
|
|
7
|
-
expect(charWidth("雾".codePointAt(0)!)).toBe(2);
|
|
8
|
-
expect(charWidth(",".codePointAt(0)!)).toBe(2); // fullwidth comma
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("treats control chars as width 0", () => {
|
|
12
|
-
expect(charWidth("\r".codePointAt(0)!)).toBe(0);
|
|
13
|
-
expect(charWidth("\n".codePointAt(0)!)).toBe(0);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("visualWidth sums correctly and ignores ANSI", () => {
|
|
17
|
-
expect(visualWidth("abc")).toBe(3);
|
|
18
|
-
expect(visualWidth("雾雨")).toBe(4);
|
|
19
|
-
expect(visualWidth("a雾b")).toBe(4);
|
|
20
|
-
expect(visualWidth("\x1b[36m雾\x1b[39m")).toBe(2); // color codes don't count
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("padVisual pads to a visual column count", () => {
|
|
24
|
-
expect(visualWidth(padVisual("雾", 6))).toBe(6);
|
|
25
|
-
expect(padVisual("abc", 2)).toBe("abc"); // never truncates
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
/** Capture writes from a StreamRenderer into a string. */
|
|
30
|
-
function render(text: string, columns = 40, chunk = 3): string {
|
|
31
|
-
let buf = "";
|
|
32
|
-
const fakeOut = { columns, write: (s: string) => { buf += s; return true; } } as any;
|
|
33
|
-
const r = new StreamRenderer(fakeOut, { gutter: " " });
|
|
34
|
-
for (let i = 0; i < text.length; i += chunk) r.write(text.slice(i, i + chunk));
|
|
35
|
-
r.flush();
|
|
36
|
-
return buf;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe("StreamRenderer", () => {
|
|
40
|
-
it("prefixes every line with the gutter", () => {
|
|
41
|
-
const out = render("hello world", 80);
|
|
42
|
-
expect(out.startsWith(" ")).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("never exceeds the content width per visual line", () => {
|
|
46
|
-
const out = render("天空织机是一个本地优先的多智能体终端框架用于验证换行宽度限制是否生效啊", 40);
|
|
47
|
-
const maxContent = Math.min(40 - 2 - 1, 96);
|
|
48
|
-
for (const line of out.split("\n")) {
|
|
49
|
-
expect(visualWidth(line)).toBeLessThanOrEqual(2 + maxContent); // gutter + content
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("strips stray carriage returns (CRLF from providers)", () => {
|
|
54
|
-
const out = render("line one\r\nline two", 80);
|
|
55
|
-
expect(out.includes("\r")).toBe(false);
|
|
56
|
-
expect(out).toContain("line one");
|
|
57
|
-
expect(out).toContain("line two");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("wraps English on word boundaries without splitting short words", () => {
|
|
61
|
-
// maxCols floors at 32, so use text long enough to exceed it.
|
|
62
|
-
const out = render("alpha beta gamma delta epsilon zeta eta theta iota kappa", 40);
|
|
63
|
-
expect(out.split("\n").length).toBeGreaterThan(1);
|
|
64
|
-
// no whole word should be broken across a wrap (each appears intact)
|
|
65
|
-
for (const w of ["alpha", "epsilon", "kappa"]) expect(out).toContain(w);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { charWidth, visualWidth, padVisual, StreamRenderer } from "../src/cli/tui";
|
|
3
|
+
|
|
4
|
+
describe("CJK-aware width", () => {
|
|
5
|
+
it("counts ascii as 1, CJK as 2", () => {
|
|
6
|
+
expect(charWidth("a".codePointAt(0)!)).toBe(1);
|
|
7
|
+
expect(charWidth("雾".codePointAt(0)!)).toBe(2);
|
|
8
|
+
expect(charWidth(",".codePointAt(0)!)).toBe(2); // fullwidth comma
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("treats control chars as width 0", () => {
|
|
12
|
+
expect(charWidth("\r".codePointAt(0)!)).toBe(0);
|
|
13
|
+
expect(charWidth("\n".codePointAt(0)!)).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("visualWidth sums correctly and ignores ANSI", () => {
|
|
17
|
+
expect(visualWidth("abc")).toBe(3);
|
|
18
|
+
expect(visualWidth("雾雨")).toBe(4);
|
|
19
|
+
expect(visualWidth("a雾b")).toBe(4);
|
|
20
|
+
expect(visualWidth("\x1b[36m雾\x1b[39m")).toBe(2); // color codes don't count
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("padVisual pads to a visual column count", () => {
|
|
24
|
+
expect(visualWidth(padVisual("雾", 6))).toBe(6);
|
|
25
|
+
expect(padVisual("abc", 2)).toBe("abc"); // never truncates
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Capture writes from a StreamRenderer into a string. */
|
|
30
|
+
function render(text: string, columns = 40, chunk = 3): string {
|
|
31
|
+
let buf = "";
|
|
32
|
+
const fakeOut = { columns, write: (s: string) => { buf += s; return true; } } as any;
|
|
33
|
+
const r = new StreamRenderer(fakeOut, { gutter: " " });
|
|
34
|
+
for (let i = 0; i < text.length; i += chunk) r.write(text.slice(i, i + chunk));
|
|
35
|
+
r.flush();
|
|
36
|
+
return buf;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("StreamRenderer", () => {
|
|
40
|
+
it("prefixes every line with the gutter", () => {
|
|
41
|
+
const out = render("hello world", 80);
|
|
42
|
+
expect(out.startsWith(" ")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("never exceeds the content width per visual line", () => {
|
|
46
|
+
const out = render("天空织机是一个本地优先的多智能体终端框架用于验证换行宽度限制是否生效啊", 40);
|
|
47
|
+
const maxContent = Math.min(40 - 2 - 1, 96);
|
|
48
|
+
for (const line of out.split("\n")) {
|
|
49
|
+
expect(visualWidth(line)).toBeLessThanOrEqual(2 + maxContent); // gutter + content
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("strips stray carriage returns (CRLF from providers)", () => {
|
|
54
|
+
const out = render("line one\r\nline two", 80);
|
|
55
|
+
expect(out.includes("\r")).toBe(false);
|
|
56
|
+
expect(out).toContain("line one");
|
|
57
|
+
expect(out).toContain("line two");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("wraps English on word boundaries without splitting short words", () => {
|
|
61
|
+
// maxCols floors at 32, so use text long enough to exceed it.
|
|
62
|
+
const out = render("alpha beta gamma delta epsilon zeta eta theta iota kappa", 40);
|
|
63
|
+
expect(out.split("\n").length).toBeGreaterThan(1);
|
|
64
|
+
// no whole word should be broken across a wrap (each appears intact)
|
|
65
|
+
for (const w of ["alpha", "epsilon", "kappa"]) expect(out).toContain(w);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/vitest.config.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
test: {
|
|
6
|
-
globals: true,
|
|
7
|
-
environment: 'node',
|
|
8
|
-
include: ['tests/**/*.test.ts'],
|
|
9
|
-
setupFiles: ['tests/setup.ts'],
|
|
10
|
-
},
|
|
11
|
-
resolve: {
|
|
12
|
-
alias: {
|
|
13
|
-
'@skyloom': path.resolve(__dirname, 'src'),
|
|
14
|
-
'@': path.resolve(__dirname, 'src'),
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
});
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
environment: 'node',
|
|
8
|
+
include: ['tests/**/*.test.ts'],
|
|
9
|
+
setupFiles: ['tests/setup.ts'],
|
|
10
|
+
},
|
|
11
|
+
resolve: {
|
|
12
|
+
alias: {
|
|
13
|
+
'@skyloom': path.resolve(__dirname, 'src'),
|
|
14
|
+
'@': path.resolve(__dirname, 'src'),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|