pi-minimal 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 +36 -0
- package/README.md +7 -4
- package/dist/config.js +19 -8
- package/dist/index.js +4 -1
- package/dist/tool-rows.js +34 -1
- package/dist/tools.js +230 -18
- package/dist/tracker.js +16 -12
- package/dist/transcript.js +74 -15
- package/package.json +4 -1
- package/release-notes.md +12 -21
- package/src/config.ts +18 -8
- package/src/index.ts +4 -1
- package/src/tool-rows.ts +38 -1
- package/src/tools.ts +220 -18
- package/src/tracker.ts +14 -9
- package/src/transcript.ts +93 -38
- package/test/codemode-rows.test.ts +1 -1
- package/test/config.test.ts +40 -19
- package/test/package.test.ts +0 -23
- package/test/tool-rows.test.ts +17 -1
- package/test/tools.test.ts +121 -3
- package/test/tracker.test.ts +18 -0
- package/test/transcript.test.ts +50 -3
package/test/config.test.ts
CHANGED
|
@@ -30,85 +30,106 @@ test("defaults hideGauntletWidgets to true", (t) => {
|
|
|
30
30
|
const { agentDir, cwd } = setup(t)
|
|
31
31
|
const error = t.mock.method(console, "error")
|
|
32
32
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
33
|
-
assert.deepEqual(config, { hideGauntletWidgets: true })
|
|
33
|
+
assert.deepEqual(config, { hideGauntletWidgets: true, editDiff: "expanded" })
|
|
34
34
|
assert.equal(error.mock.callCount(), 0)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
test("global false overrides default", (t) => {
|
|
38
38
|
const { agentDir, cwd } = setup(t)
|
|
39
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
39
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
40
40
|
const error = t.mock.method(console, "error")
|
|
41
41
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: false })
|
|
42
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
42
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
43
43
|
assert.equal(error.mock.callCount(), 0)
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
test("trusted project true overrides global false", (t) => {
|
|
47
47
|
const { agentDir, cwd } = setup(t)
|
|
48
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
49
|
-
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: true }))
|
|
48
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
49
|
+
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: true, editDiff: "expanded" }))
|
|
50
50
|
const error = t.mock.method(console, "error")
|
|
51
51
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
52
|
-
assert.deepEqual(config, { hideGauntletWidgets: true })
|
|
52
|
+
assert.deepEqual(config, { hideGauntletWidgets: true, editDiff: "expanded" })
|
|
53
53
|
assert.equal(error.mock.callCount(), 0)
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
test("untrusted project false does not override global true", (t) => {
|
|
57
57
|
const { agentDir, cwd } = setup(t)
|
|
58
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: true }))
|
|
59
|
-
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: false }))
|
|
58
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: true, editDiff: "expanded" }))
|
|
59
|
+
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
60
60
|
const error = t.mock.method(console, "error")
|
|
61
61
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: false })
|
|
62
|
-
assert.deepEqual(config, { hideGauntletWidgets: true })
|
|
62
|
+
assert.deepEqual(config, { hideGauntletWidgets: true, editDiff: "expanded" })
|
|
63
63
|
assert.equal(error.mock.callCount(), 0)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
test("omitted keys do not override", (t) => {
|
|
67
67
|
const { agentDir, cwd } = setup(t)
|
|
68
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
68
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
69
69
|
writeProject(cwd, JSON.stringify({}))
|
|
70
70
|
const error = t.mock.method(console, "error")
|
|
71
71
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
72
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
72
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
73
73
|
assert.equal(error.mock.callCount(), 0)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
test("unknown keys do not override", (t) => {
|
|
77
77
|
const { agentDir, cwd } = setup(t)
|
|
78
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
78
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
79
79
|
writeProject(cwd, JSON.stringify({ extra: true }))
|
|
80
80
|
const error = t.mock.method(console, "error")
|
|
81
81
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
82
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
82
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
83
83
|
assert.equal(error.mock.callCount(), 0)
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
test("invalid JSON calls console.error and leaves the lower-precedence value unchanged", (t) => {
|
|
87
87
|
const { agentDir, cwd } = setup(t)
|
|
88
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
88
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
89
89
|
writeProject(cwd, "{")
|
|
90
90
|
const error = t.mock.method(console, "error")
|
|
91
91
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
92
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
92
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
93
93
|
assert.ok(error.mock.callCount() >= 1)
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
test("non-object root calls console.error and leaves the lower-precedence value unchanged", (t) => {
|
|
97
97
|
const { agentDir, cwd } = setup(t)
|
|
98
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
98
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
99
99
|
writeProject(cwd, JSON.stringify(["hideGauntletWidgets", false]))
|
|
100
100
|
const error = t.mock.method(console, "error")
|
|
101
101
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
102
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
102
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
103
103
|
assert.ok(error.mock.callCount() >= 1)
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
test("non-boolean hideGauntletWidgets calls console.error and leaves the lower-precedence value unchanged", (t) => {
|
|
107
107
|
const { agentDir, cwd } = setup(t)
|
|
108
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
108
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
109
109
|
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: "yes" }))
|
|
110
110
|
const error = t.mock.method(console, "error")
|
|
111
111
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
112
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
112
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
113
113
|
assert.ok(error.mock.callCount() >= 1)
|
|
114
114
|
})
|
|
115
|
+
|
|
116
|
+
test("editDiff accepts always, never and expanded, layered like other keys", (t) => {
|
|
117
|
+
const { agentDir, cwd } = setup(t)
|
|
118
|
+
writeGlobal(agentDir, JSON.stringify({ editDiff: "always" }))
|
|
119
|
+
writeProject(cwd, JSON.stringify({ editDiff: "never" }))
|
|
120
|
+
const error = t.mock.method(console, "error")
|
|
121
|
+
assert.equal(loadPiMinimalConfig({ agentDir, cwd, projectTrusted: false }).editDiff, "always")
|
|
122
|
+
assert.equal(loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true }).editDiff, "never")
|
|
123
|
+
assert.equal(error.mock.callCount(), 0)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("an invalid editDiff value is reported and does not override", (t) => {
|
|
127
|
+
const { agentDir, cwd } = setup(t)
|
|
128
|
+
writeGlobal(agentDir, JSON.stringify({ editDiff: "always", hideGauntletWidgets: false }))
|
|
129
|
+
writeProject(cwd, JSON.stringify({ editDiff: "sometimes" }))
|
|
130
|
+
const error = t.mock.method(console, "error")
|
|
131
|
+
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
132
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "always" })
|
|
133
|
+
assert.equal(error.mock.callCount(), 1)
|
|
134
|
+
assert.match(String(error.mock.calls[0]!.arguments[0]), /editDiff must be one of "always", "never", "expanded"/)
|
|
135
|
+
})
|
package/test/package.test.ts
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict"
|
|
2
|
-
import test from "node:test"
|
|
3
|
-
import { readFileSync } from "node:fs"
|
|
4
|
-
|
|
5
|
-
test("package manifest exposes the committed extension", () => {
|
|
6
|
-
const pkg = JSON.parse(readFileSync("package.json", "utf8"))
|
|
7
|
-
assert.equal(pkg.name, "pi-minimal")
|
|
8
|
-
assert.equal(pkg.version, "0.3.0")
|
|
9
|
-
const lock = JSON.parse(readFileSync("package-lock.json", "utf8"))
|
|
10
|
-
assert.equal(lock.version, pkg.version, "lockfile root version matches package")
|
|
11
|
-
assert.equal(lock.packages[""].version, pkg.version, "lockfile package version matches package")
|
|
12
|
-
assert.ok(readFileSync("CHANGELOG.md", "utf8").includes(`## [${pkg.version}] - `), "version has a dated release entry")
|
|
13
|
-
assert.equal(pkg.license, "MIT")
|
|
14
|
-
assert.equal(pkg.type, "module")
|
|
15
|
-
assert.deepEqual(pkg.engines, { node: ">=22.19.0" })
|
|
16
|
-
assert.deepEqual(pkg.keywords, ["pi-package"])
|
|
17
|
-
assert.deepEqual(pkg.repository, { type: "git", url: "git+https://github.com/raviy06/pi-minimal.git" })
|
|
18
|
-
assert.deepEqual(pkg.pi, { extensions: ["./dist/index.js"], themes: ["./themes/minimal-dark.json"] })
|
|
19
|
-
assert.deepEqual(pkg.peerDependencies, {
|
|
20
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
21
|
-
"@earendil-works/pi-tui": "*",
|
|
22
|
-
})
|
|
23
|
-
})
|
package/test/tool-rows.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
formatOutcome,
|
|
9
9
|
layoutRow,
|
|
10
10
|
rowGlyph,
|
|
11
|
+
setSessionCwd,
|
|
11
12
|
shortName,
|
|
12
13
|
toolItem,
|
|
13
14
|
} from "../src/tool-rows.js"
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
const text = (s: string) => ({ content: [{ type: "text", text: s }] })
|
|
16
17
|
|
|
17
18
|
test("toolItem picks the first command line for bash and a two-segment path otherwise", () => {
|
|
18
|
-
assert.equal(toolItem("bash", { command: "git status
|
|
19
|
+
assert.equal(toolItem("bash", { command: "git status" }), "git status")
|
|
19
20
|
assert.equal(toolItem("read", { path: "/a/b/c/file.ts" }), "c/file.ts")
|
|
20
21
|
assert.equal(toolItem("grep", { pattern: "foo" }), "foo")
|
|
21
22
|
assert.equal(toolItem("ls", {}), "ls")
|
|
@@ -107,3 +108,18 @@ test("layoutRow drops the outcome when the row is too narrow", () => {
|
|
|
107
108
|
assert.equal(row.outcome, "")
|
|
108
109
|
assert.equal(row.gap, 0)
|
|
109
110
|
})
|
|
111
|
+
|
|
112
|
+
test("bash items drop cd into the session cwd, shorten other cd targets, and count extra lines", () => {
|
|
113
|
+
setSessionCwd("/Users/me/repo", "/Users/me")
|
|
114
|
+
try {
|
|
115
|
+
assert.equal(toolItem("bash", { command: "cd /Users/me/repo && npm test" }), "npm test")
|
|
116
|
+
assert.equal(toolItem("bash", { command: "cd ~/repo/ && npm test" }), "npm test")
|
|
117
|
+
assert.equal(toolItem("bash", { command: "cd ~/Dev/src/github.com/raviy06/pi-subagents && sed -n 1,2p a.ts" }), "pi-subagents › sed -n 1,2p a.ts")
|
|
118
|
+
assert.equal(toolItem("bash", { command: 'cd "/tmp/with space" && ls' }), "with space › ls")
|
|
119
|
+
assert.equal(toolItem("bash", { command: "cd /tmp" }), "cd /tmp", "a bare cd stays as written")
|
|
120
|
+
assert.equal(toolItem("bash", { command: "cd ~/repo && python3 - <<'EOF'\nprint(1)\n\nprint(2)\nEOF" }), "python3 - <<'EOF' · +3 lines")
|
|
121
|
+
assert.equal(toolItem("bash", { command: "git status\ngit diff" }), "git status · +1 line")
|
|
122
|
+
} finally {
|
|
123
|
+
setSessionCwd(undefined)
|
|
124
|
+
}
|
|
125
|
+
})
|
package/test/tools.test.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import assert from "node:assert/strict"
|
|
2
2
|
import test from "node:test"
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
codemodeRenderers,
|
|
6
|
+
compactRenderers,
|
|
7
|
+
genericItem,
|
|
8
|
+
genericOutcome,
|
|
9
|
+
genericRenderers,
|
|
10
|
+
genericVerb,
|
|
11
|
+
installToolRenderers,
|
|
12
|
+
isToolKind,
|
|
13
|
+
setEditDiffMode,
|
|
14
|
+
stopLiveWatch,
|
|
15
|
+
} from "../src/tools.js"
|
|
5
16
|
|
|
6
17
|
test("isToolKind recognises the built-in tools only", () => {
|
|
7
18
|
for (const name of ["bash", "read", "edit", "write", "grep", "find", "ls"]) assert.equal(isToolKind(name), true)
|
|
@@ -19,7 +30,12 @@ test("installToolRenderers registers a resolver at load time that only claims bu
|
|
|
19
30
|
installToolRenderers(pi)
|
|
20
31
|
assert.ok(resolver, "resolver registered synchronously, before session_start")
|
|
21
32
|
const fallback = { renderShell: "default" }
|
|
22
|
-
|
|
33
|
+
const own = { renderCall() {}, renderResult() {} }
|
|
34
|
+
assert.equal(resolver!("subagent", () => own), own, "tools with their own renderer keep it")
|
|
35
|
+
const generic = resolver!("mcp__foo__bar", () => fallback) as { renderShell?: string; renderCall?: unknown }
|
|
36
|
+
assert.equal(generic.renderShell, "self", "renderer-less tools get the compact row")
|
|
37
|
+
assert.equal(typeof generic.renderCall, "function")
|
|
38
|
+
assert.equal((resolver!("mcp__foo__bar", () => undefined) as { renderShell?: string }).renderShell, "self")
|
|
23
39
|
const codemode = resolver!("codemode", () => fallback) as { renderShell?: string }
|
|
24
40
|
assert.equal(codemode.renderShell, "self")
|
|
25
41
|
const mine = resolver!("bash", () => fallback) as { renderShell?: string }
|
|
@@ -90,7 +106,7 @@ test("codemode renders one parent row plus one child row per nested call, no scr
|
|
|
90
106
|
assert.equal(lines.length, 3)
|
|
91
107
|
assert.match(lines[0], /✓ code\s+2 calls · plan_tracker, bash\s+2 lines · 1.2s$/)
|
|
92
108
|
assert.match(lines[1], /✓ plan_tracker action=update index=6 status=complete\s+0.3s$/)
|
|
93
|
-
assert.match(lines[2], /✓ bash\s+
|
|
109
|
+
assert.match(lines[2], /✓ bash\s+r › sed -n 1,2p a.md\s+0.6s$/)
|
|
94
110
|
assert.ok(lines.every((line) => !line.includes("await")), "script hidden when collapsed")
|
|
95
111
|
assert.deepEqual(body.render(100), [])
|
|
96
112
|
for (const line of lines) assert.ok(line.length <= 100)
|
|
@@ -123,3 +139,105 @@ test("codemode shows nested calls live while the script is still running", () =>
|
|
|
123
139
|
assert.equal((context.state as { done?: boolean }).done, undefined)
|
|
124
140
|
stopLiveWatch()
|
|
125
141
|
})
|
|
142
|
+
|
|
143
|
+
test("generic rows label MCP and agent tools and read arguments as key=value", () => {
|
|
144
|
+
assert.equal(genericVerb("mcp__re_mcp__query_prometheus"), "mcp")
|
|
145
|
+
assert.equal(genericVerb("subagents_enable"), "agent")
|
|
146
|
+
assert.equal(genericVerb("todo"), "tool")
|
|
147
|
+
assert.equal(genericItem("mcp__re_mcp__query_prometheus", { query: "sum(rate(x[5m]))", limit: 5 }), "re_mcp · query_prometheus query=sum(rate(x[5m])) limit=5")
|
|
148
|
+
assert.equal(genericItem("subagents_enable", {}), "subagents_enable")
|
|
149
|
+
assert.equal(genericItem("todo", { action: "add", text: "line one\nline two" }), "todo action=add text=line one")
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("generic outcomes show a short first line verbatim, otherwise a line count", () => {
|
|
153
|
+
const text = (s: string) => ({ content: [{ type: "text", text: s }] })
|
|
154
|
+
assert.deepEqual(genericOutcome(text("Updated (9/9 done)"), false, 300), { text: "Updated (9/9 done) · 0.3s" })
|
|
155
|
+
assert.deepEqual(genericOutcome(text("Enabled: subagent. If your tool list includes subagent, call it now."), false, 0), { text: "1 line · 0s" })
|
|
156
|
+
assert.deepEqual(genericOutcome(text("a\nb\n"), false, 1400), { text: "2 lines · 1.4s" })
|
|
157
|
+
assert.deepEqual(genericOutcome(text(""), false, undefined), { text: "ok" })
|
|
158
|
+
assert.deepEqual(genericOutcome(text("\nboom\nmore"), true, 100), { text: "failed · 0.1s", errorLine: "boom" })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test("generic renderer draws one row, an error line on failure, and output on expand", () => {
|
|
162
|
+
const r = genericRenderers("mcp__re_mcp__query_prometheus")
|
|
163
|
+
const make = (extra: Record<string, unknown> = {}) => ({ toolCallId: "g1", invalidate() {}, state: {}, isPartial: false, isError: false, durationMs: 1400, ...extra })
|
|
164
|
+
const args = { query: "up" }
|
|
165
|
+
let context = make()
|
|
166
|
+
r.renderResult!({ content: [{ type: "text", text: "a\nb\nc" }] } as never, { expanded: false, isPartial: false } as never, plainTheme, context as never)
|
|
167
|
+
const [line, ...rest] = r.renderCall!(args as never, plainTheme, context as never).render(90)
|
|
168
|
+
assert.match(line, /✓ mcp\s+re_mcp · query_prometheus query=up\s+3 lines · 1.4s$/)
|
|
169
|
+
assert.equal(rest.length, 0)
|
|
170
|
+
|
|
171
|
+
context = make({ isError: true })
|
|
172
|
+
const failed = r.renderResult!({ content: [{ type: "text", text: "401 unauthorized" }] } as never, { expanded: false, isPartial: false } as never, plainTheme, context as never)
|
|
173
|
+
assert.match(r.renderCall!(args as never, plainTheme, context as never).render(90)[0], /✗ mcp .*failed · 1.4s$/)
|
|
174
|
+
assert.match(failed.render(90).join(""), /│ 401 unauthorized/)
|
|
175
|
+
|
|
176
|
+
context = make({ expanded: true })
|
|
177
|
+
const open = r.renderResult!({ content: [{ type: "text", text: "a\nb" }] } as never, { expanded: true, isPartial: false } as never, plainTheme, context as never)
|
|
178
|
+
assert.deepEqual(open.render(90).map((l) => l.trim()), ["│ a", "│ b"])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// pi's edit tool emits its own line-numbered form, not a unified diff: "+123 content", "-123 content", " 123 context", "..." between hunks.
|
|
182
|
+
const EDIT_DIFF = " 1 context\n-2 const a = old(x)\n+2 const a = fresh(x)\n 3 context"
|
|
183
|
+
const editResult = { content: [{ type: "text", text: "Successfully replaced 1 block(s)" }], details: { diff: EDIT_DIFF } }
|
|
184
|
+
function editRows(expanded: boolean, result: unknown = editResult, isError = false): string[] {
|
|
185
|
+
const renderers = compactRenderers("edit")
|
|
186
|
+
const context = { args: { path: "a.ts" }, toolCallId: "e1", invalidate() {}, state: {}, isPartial: false, isError, durationMs: 100 } as never
|
|
187
|
+
const theme = {
|
|
188
|
+
fg: (c: string, s: string) => (c.startsWith("toolDiff") ? `<${c.slice(8).toLowerCase()}>${s}` : s),
|
|
189
|
+
inverse: (s: string) => `[${s}]`,
|
|
190
|
+
} as never
|
|
191
|
+
const block = renderers.renderResult!(result as never, { expanded, isPartial: false } as never, theme, context)
|
|
192
|
+
return block.render(80).map((line) => line.trimEnd())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
test("editDiff=expanded (default) shows the diff only behind ctrl+o, in the theme's diff colours", (t) => {
|
|
196
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
197
|
+
setEditDiffMode("expanded")
|
|
198
|
+
assert.deepEqual(editRows(false), [])
|
|
199
|
+
const rows = editRows(true)
|
|
200
|
+
assert.equal(rows.length, 4)
|
|
201
|
+
assert.match(rows[0]!, /│ <context> 1 context$/)
|
|
202
|
+
assert.match(rows[1]!, /│ <removed>-2 const a = /)
|
|
203
|
+
assert.match(rows[2]!, /│ <added>\+2 const a = /)
|
|
204
|
+
assert.ok(rows.every((row) => !row.includes("Successfully replaced")), "the diff replaces the result text")
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test("a one-line replacement highlights only the changed tokens with inverse", (t) => {
|
|
208
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
209
|
+
setEditDiffMode("expanded")
|
|
210
|
+
const [, removed, added] = editRows(true)
|
|
211
|
+
assert.match(removed!, /-2 const a = \[old\]\(x\)$/)
|
|
212
|
+
assert.match(added!, /\+2 const a = \[fresh\]\(x\)$/)
|
|
213
|
+
const multi = editRows(true, { content: [], details: { diff: "-1 a\n-2 b\n+1 c" } })
|
|
214
|
+
assert.ok(multi.every((row) => !row.includes("[")), "blocks that are not 1:1 are not token-highlighted")
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test("editDiff=always shows the diff inline and editDiff=never only on request falls back to the result text", (t) => {
|
|
218
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
219
|
+
setEditDiffMode("always")
|
|
220
|
+
assert.equal(editRows(false).length, 4)
|
|
221
|
+
assert.equal(editRows(true).length, 4)
|
|
222
|
+
setEditDiffMode("never")
|
|
223
|
+
assert.deepEqual(editRows(false), [])
|
|
224
|
+
assert.match(editRows(true)[0]!, /│ Successfully replaced 1 block\(s\)$/)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test("editDiff=always truncates a long inline diff and leaves the full diff to ctrl+o", (t) => {
|
|
228
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
229
|
+
setEditDiffMode("always")
|
|
230
|
+
const long = { content: [{ type: "text", text: "ok" }], details: { diff: Array.from({ length: 60 }, (_, i) => `+${i + 1} line ${i}`).join("\n") } }
|
|
231
|
+
const inline = editRows(false, long)
|
|
232
|
+
assert.equal(inline.length, 41)
|
|
233
|
+
assert.match(inline[40]!, /… 20 more lines \(ctrl\+o\)$/)
|
|
234
|
+
assert.equal(editRows(true, long).length, 60)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test("edit failures and results without a diff never render a diff block", (t) => {
|
|
238
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
239
|
+
setEditDiffMode("always")
|
|
240
|
+
assert.deepEqual(editRows(false, { content: [{ type: "text", text: "edited" }] }), [])
|
|
241
|
+
const failed = editRows(false, { content: [{ type: "text", text: "oldText not found" }] }, true)
|
|
242
|
+
assert.ok(failed.every((row) => !row.includes("<removed>")))
|
|
243
|
+
})
|
package/test/tracker.test.ts
CHANGED
|
@@ -102,3 +102,21 @@ test("hideGauntletWidgets clears phase_tracker and plan_tracker", () => {
|
|
|
102
102
|
["plan_tracker", undefined],
|
|
103
103
|
])
|
|
104
104
|
})
|
|
105
|
+
|
|
106
|
+
test("task line is a segmented bar coloured by status, scaled past 12 tasks", () => {
|
|
107
|
+
const colour = { fg: (name: string, s: string) => `<${name}>${s}` }
|
|
108
|
+
const tasks = [
|
|
109
|
+
{ name: "a", status: "complete" },
|
|
110
|
+
{ name: "b", status: "failed" },
|
|
111
|
+
{ name: "c", status: "in_progress" },
|
|
112
|
+
{ name: "d", status: "pending" },
|
|
113
|
+
] as never
|
|
114
|
+
const [line] = formatTrackerLines(ctxFrom({ tasks }), colour)
|
|
115
|
+
assert.ok(line.includes("<success>▰<error>▰<warning>▰<dim>▱"), line)
|
|
116
|
+
assert.ok(line.includes("1/4"), line)
|
|
117
|
+
assert.ok(!line.includes("✓"), "no check-mark run")
|
|
118
|
+
const many = Array.from({ length: 40 }, (_, i) => ({ name: `t${i}`, status: i < 20 ? "complete" : "pending" })) as never
|
|
119
|
+
const [scaled] = formatTrackerLines(ctxFrom({ tasks: many }), theme)
|
|
120
|
+
assert.equal((scaled.match(/[▰▱]/g) ?? []).length, 10)
|
|
121
|
+
assert.ok(scaled.includes("▰▰▰▰▰▱▱▱▱▱"), scaled)
|
|
122
|
+
})
|
package/test/transcript.test.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict"
|
|
2
2
|
import test from "node:test"
|
|
3
|
-
import {
|
|
3
|
+
import { stripVTControlCharacters } from "node:util"
|
|
4
|
+
import { Markdown, visibleWidth } from "@earendil-works/pi-tui"
|
|
5
|
+
import { gutterThinking, wrapLine } from "../src/transcript.js"
|
|
4
6
|
|
|
5
|
-
test("gutterThinking prefixes
|
|
7
|
+
test("gutterThinking prefixes every line of a plain paragraph", () => {
|
|
6
8
|
const input = "First thought\ncontinues here.\n\nSecond thought."
|
|
7
|
-
assert.equal(gutterThinking(input), "┊ First thought\
|
|
9
|
+
assert.equal(gutterThinking(input), "┊ First thought\n┊ continues here.\n\n┊ Second thought.")
|
|
8
10
|
})
|
|
9
11
|
|
|
10
12
|
test("gutterThinking leaves block syntax and fenced code alone", () => {
|
|
@@ -15,8 +17,53 @@ test("gutterThinking leaves block syntax and fenced code alone", () => {
|
|
|
15
17
|
)
|
|
16
18
|
})
|
|
17
19
|
|
|
20
|
+
test("a list or quote interrupting a paragraph keeps its syntax", () => {
|
|
21
|
+
assert.equal(gutterThinking("Plan:\n- one\n- two\n\nDone."), "┊ Plan:\n- one\n- two\n\n┊ Done.")
|
|
22
|
+
assert.equal(gutterThinking("Note\n> quoted\nlazy line"), "┊ Note\n> quoted\nlazy line")
|
|
23
|
+
})
|
|
24
|
+
|
|
18
25
|
test("gutterThinking is idempotent and handles empty input", () => {
|
|
19
26
|
assert.equal(gutterThinking(""), "")
|
|
20
27
|
const once = gutterThinking("a\n\nb")
|
|
21
28
|
assert.equal(gutterThinking(once), once)
|
|
29
|
+
const wrapped = gutterThinking("one two three four five six seven eight nine ten eleven", 24)
|
|
30
|
+
assert.equal(gutterThinking(wrapped, 24), wrapped)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("with a width, paragraphs are pre-wrapped so wrapped lines carry the gutter", () => {
|
|
34
|
+
const out = gutterThinking("alpha beta gamma delta epsilon zeta eta theta iota kappa", 24)
|
|
35
|
+
assert.deepEqual(out.split("\n"), ["┊ alpha beta gamma delta", "┊ epsilon zeta eta theta", "┊ iota kappa"])
|
|
36
|
+
for (const line of out.split("\n")) assert.ok(visibleWidth(line) <= 24, line)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("narrow or unknown widths fall back to prefixing source lines", () => {
|
|
40
|
+
const long = "alpha beta gamma delta epsilon zeta eta theta"
|
|
41
|
+
assert.equal(gutterThinking(long, 10), `┊ ${long}`)
|
|
42
|
+
assert.equal(gutterThinking(long), `┊ ${long}`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("wrapLine keeps inline code whole and splits over-wide words", () => {
|
|
46
|
+
assert.deepEqual(wrapLine("use `ctx.ui.confirm here` now", 12), ["use", "`ctx.ui.confirm here`", "now"])
|
|
47
|
+
assert.deepEqual(wrapLine("see https://example.com/a/very/long/path", 12), ["see", "https://exam", "ple.com/a/ve", "ry/long/path"])
|
|
48
|
+
assert.deepEqual(wrapLine("", 12), [""])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("rendered through pi-tui Markdown, every line has the gutter and fits the width", () => {
|
|
52
|
+
const theme = new Proxy({}, { get: () => (s: string) => s }) as never
|
|
53
|
+
const thinking =
|
|
54
|
+
"There's an existing **/subagents** admin TUI with field editing, including `override removal`. " +
|
|
55
|
+
"I should check what it covers so the roster feature doesn't duplicate it — maybe I can hook into it instead, " +
|
|
56
|
+
"like adding model/fallback editing there.\n\nThen the `/roster` command."
|
|
57
|
+
for (const width of [40, 80, 120]) {
|
|
58
|
+
const md = new Markdown(thinking, 0, 0, theme, { color: (s: string) => s, italic: true }, {
|
|
59
|
+
transform: (text: string, available: number) => gutterThinking(text, available),
|
|
60
|
+
} as never)
|
|
61
|
+
const lines = md.render(width).map((line) => stripVTControlCharacters(line).trimEnd())
|
|
62
|
+
const content = lines.filter((line) => line.length > 0)
|
|
63
|
+
assert.ok(content.length >= 3, `width ${width}: ${JSON.stringify(lines)}`)
|
|
64
|
+
for (const line of content) {
|
|
65
|
+
assert.ok(line.startsWith("┊ "), `width ${width}: ${JSON.stringify(line)}`)
|
|
66
|
+
assert.ok(visibleWidth(line) <= width, `width ${width}: ${line}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
22
69
|
})
|