jfl 0.6.1 → 0.6.3
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/package.json
CHANGED
|
@@ -14,6 +14,12 @@ import { execSync } from "child_process"
|
|
|
14
14
|
import type { PiContext, PiTheme } from "./types.js"
|
|
15
15
|
import { hudRenderCall, hudRenderResult } from "./tool-renderers.js"
|
|
16
16
|
|
|
17
|
+
function clipLine(text: string, maxW: number): string {
|
|
18
|
+
const stripped = text.replace(/\x1b\[[0-9;]*m/g, "")
|
|
19
|
+
if (stripped.length <= maxW) return text
|
|
20
|
+
return text.slice(0, maxW - 1) + "…"
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
let projectRoot = ""
|
|
18
24
|
|
|
19
25
|
function getDaysToLaunch(root: string): string | null {
|
|
@@ -51,6 +57,22 @@ function getProjectPhase(root: string): string {
|
|
|
51
57
|
return "unknown"
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
function getLastJournalTitle(root: string): string | null {
|
|
61
|
+
try {
|
|
62
|
+
const journalDir = join(root, ".jfl", "journal")
|
|
63
|
+
if (!existsSync(journalDir)) return null
|
|
64
|
+
const files = execSync(`ls -t "${journalDir}"/*.jsonl 2>/dev/null | head -1`, {
|
|
65
|
+
cwd: root, timeout: 3000, encoding: "utf-8",
|
|
66
|
+
}).trim()
|
|
67
|
+
if (!files) return null
|
|
68
|
+
const content = readFileSync(files, "utf-8").trim()
|
|
69
|
+
const lastLine = content.split("\n").pop()
|
|
70
|
+
if (!lastLine) return null
|
|
71
|
+
const entry = JSON.parse(lastLine)
|
|
72
|
+
return entry.title ?? null
|
|
73
|
+
} catch { return null }
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
function buildHudLines(root: string): string[] {
|
|
55
77
|
const configPath = join(root, ".jfl", "config.json")
|
|
56
78
|
let projectName = root.split("/").pop() ?? "JFL"
|
|
@@ -66,41 +88,30 @@ function buildHudLines(root: string): string[] {
|
|
|
66
88
|
|
|
67
89
|
const days = getDaysToLaunch(root)
|
|
68
90
|
const phase = getProjectPhase(root)
|
|
91
|
+
const lastWork = getLastJournalTitle(root)
|
|
69
92
|
|
|
70
93
|
const lines = [
|
|
71
|
-
`◆ ${projectName}
|
|
94
|
+
`◆ ${projectName}`,
|
|
95
|
+
phase !== "unknown" ? ` ${phase}` : "",
|
|
72
96
|
days ? ` ${days}` : "",
|
|
73
|
-
|
|
97
|
+
lastWork ? ` Last: ${lastWork.slice(0, 60)}` : "",
|
|
74
98
|
].filter(Boolean)
|
|
75
99
|
|
|
76
|
-
try {
|
|
77
|
-
const crmOutput = execSync("./crm list --compact 2>/dev/null | head -3", {
|
|
78
|
-
cwd: root,
|
|
79
|
-
timeout: 5000,
|
|
80
|
-
encoding: "utf-8",
|
|
81
|
-
}).trim()
|
|
82
|
-
if (crmOutput) {
|
|
83
|
-
lines.push(" Pipeline:", ...crmOutput.split("\n").map(l => ` ${l}`))
|
|
84
|
-
}
|
|
85
|
-
} catch {}
|
|
86
|
-
|
|
87
100
|
return lines
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
export function setupHudTool(ctx: PiContext): void {
|
|
91
104
|
projectRoot = ctx.session.projectRoot
|
|
92
105
|
|
|
93
|
-
// Themed widget above editor
|
|
94
106
|
ctx.ui.setWidget("jfl-hud", (_tui: any, theme: PiTheme) => {
|
|
95
107
|
const lines = buildHudLines(projectRoot)
|
|
96
108
|
const themed = lines.map((line, i) => {
|
|
97
|
-
if (i === 0) return theme.fg("accent", line)
|
|
98
|
-
if (line.includes("
|
|
99
|
-
if (line.includes("Pipeline")) return theme.fg("accent", line)
|
|
109
|
+
if (i === 0) return theme.fg("accent", theme.bold(line))
|
|
110
|
+
if (line.includes("Last:")) return theme.fg("dim", line)
|
|
100
111
|
return theme.fg("muted", line)
|
|
101
112
|
})
|
|
102
113
|
return {
|
|
103
|
-
render: () => themed,
|
|
114
|
+
render: (width: number) => themed.map(l => clipLine(l, width)),
|
|
104
115
|
invalidate() {},
|
|
105
116
|
}
|
|
106
117
|
})
|
|
@@ -135,11 +146,10 @@ export async function updateHudWidget(ctx: PiContext): Promise<void> {
|
|
|
135
146
|
ctx.ui.setWidget("jfl-hud", (_tui: any, theme: PiTheme) => {
|
|
136
147
|
const lines = buildHudLines(projectRoot)
|
|
137
148
|
const themed = lines.map((line, i) => {
|
|
138
|
-
if (i === 0) return theme.fg("accent", line)
|
|
139
|
-
if (line.includes("
|
|
140
|
-
if (line.includes("Pipeline")) return theme.fg("accent", line)
|
|
149
|
+
if (i === 0) return theme.fg("accent", theme.bold(line))
|
|
150
|
+
if (line.includes("Last:")) return theme.fg("dim", line)
|
|
141
151
|
return theme.fg("muted", line)
|
|
142
152
|
})
|
|
143
|
-
return { render: () => themed, invalidate() {} }
|
|
153
|
+
return { render: (width: number) => themed.map(l => clipLine(l, width)), invalidate() {} }
|
|
144
154
|
})
|
|
145
155
|
}
|
|
@@ -317,24 +317,26 @@ export default async function jflExtension(pi: any): Promise<void> {
|
|
|
317
317
|
|
|
318
318
|
ctx.log(`JFL: ${projectName} — session ready`)
|
|
319
319
|
|
|
320
|
-
if (config.pi?.auto_start !== false && pi.
|
|
320
|
+
if (config.pi?.auto_start !== false && pi.sendMessage) {
|
|
321
321
|
setTimeout(() => {
|
|
322
|
-
pi.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
322
|
+
pi.sendMessage({
|
|
323
|
+
customType: "jfl-session-init",
|
|
324
|
+
content: [
|
|
325
|
+
`JFL session ready: "${projectName}" on branch ${ctx.session.branch}.`,
|
|
326
|
+
"",
|
|
327
|
+
"You have full project context injected in your system prompt (CLAUDE.md + recent journal + knowledge docs).",
|
|
328
|
+
"Tools available: jfl_context, jfl_memory_search, jfl_hud, jfl_journal.",
|
|
329
|
+
"",
|
|
330
|
+
"Greet the user naturally with a brief status:",
|
|
331
|
+
"- What was worked on recently (from your injected context)",
|
|
332
|
+
"- Current phase and any blockers",
|
|
333
|
+
"- A suggested next action",
|
|
334
|
+
"",
|
|
335
|
+
"Keep it to 3-5 lines. No setup noise. Just be ready to work.",
|
|
336
|
+
"Write journal entries as you work. Capture decisions immediately.",
|
|
337
|
+
].join("\n"),
|
|
338
|
+
display: false,
|
|
339
|
+
}, { triggerTurn: true, deliverAs: "steer" })
|
|
338
340
|
}, 500)
|
|
339
341
|
}
|
|
340
342
|
})
|
|
@@ -12,13 +12,14 @@ import type { PiTheme } from "./types.js"
|
|
|
12
12
|
|
|
13
13
|
// ─── Shared helpers ──────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
|
+
const MAX_LINE_W = 140
|
|
16
|
+
|
|
15
17
|
function visibleLen(text: string): number {
|
|
16
18
|
return text.replace(/\x1b\[[0-9;]*m/g, "").length
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function truncLine(text: string, maxW: number): string {
|
|
21
|
+
function truncLine(text: string, maxW: number = MAX_LINE_W): string {
|
|
20
22
|
if (visibleLen(text) <= maxW) return text
|
|
21
|
-
// Walk char-by-char, skipping ANSI sequences, until we hit maxW visible chars
|
|
22
23
|
let visible = 0
|
|
23
24
|
let i = 0
|
|
24
25
|
while (i < text.length && visible < maxW - 1) {
|
|
@@ -32,6 +33,10 @@ function truncLine(text: string, maxW: number): string {
|
|
|
32
33
|
return text.slice(0, i) + "\x1b[0m…"
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function safeRender(lines: string[]): string[] {
|
|
37
|
+
return lines.map(l => truncLine(l, MAX_LINE_W))
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
function wrapText(text: string, width: number): string[] {
|
|
36
41
|
const lines: string[] = []
|
|
37
42
|
for (const raw of text.split("\n")) {
|
|
@@ -55,7 +60,7 @@ function sectionBorder(theme: PiTheme, label: string, width: number): string {
|
|
|
55
60
|
|
|
56
61
|
export function hudRenderCall(args: Record<string, any>, theme: PiTheme): any {
|
|
57
62
|
const label = theme.fg("accent", "◆") + " " + theme.fg("toolTitle", theme.bold("HUD"))
|
|
58
|
-
return { render: () => [label], invalidate() {} }
|
|
63
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
export function hudRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -65,7 +70,7 @@ export function hudRenderResult(result: any, opts: { expanded: boolean }, theme:
|
|
|
65
70
|
if (!opts.expanded && lines.length > 5) {
|
|
66
71
|
const display = lines.slice(0, 5).map(l => theme.fg("toolOutput", l))
|
|
67
72
|
display.push(theme.fg("dim", `... ${lines.length - 5} more lines (Ctrl+O)`))
|
|
68
|
-
return { render: () => display, invalidate() {} }
|
|
73
|
+
return { render: () => safeRender(display), invalidate() {} }
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
const themed = lines.map(line => {
|
|
@@ -75,7 +80,7 @@ export function hudRenderResult(result: any, opts: { expanded: boolean }, theme:
|
|
|
75
80
|
return theme.fg("toolOutput", line)
|
|
76
81
|
})
|
|
77
82
|
|
|
78
|
-
return { render: () => themed, invalidate() {} }
|
|
83
|
+
return { render: () => safeRender(themed), invalidate() {} }
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
// ─── Context Tool ───────────────────────────────────────────────────────────
|
|
@@ -83,48 +88,45 @@ export function hudRenderResult(result: any, opts: { expanded: boolean }, theme:
|
|
|
83
88
|
export function contextRenderCall(args: Record<string, any>, theme: PiTheme): any {
|
|
84
89
|
const query = args.query ?? ""
|
|
85
90
|
const label = theme.fg("toolTitle", theme.bold("context ")) + theme.fg("accent", `"${query}"`)
|
|
86
|
-
return { render: () => [label], invalidate() {} }
|
|
91
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
export function contextRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
90
|
-
const MAX_W = 140
|
|
91
95
|
const raw = extractText(result)
|
|
92
|
-
if (raw === "No relevant context found." || raw === "No context available.") {
|
|
93
|
-
return { render: () => [theme.fg("dim", "No
|
|
96
|
+
if (raw === "No relevant context found." || raw === "No context available." || raw === "Context Hub unavailable.") {
|
|
97
|
+
return { render: () => safeRender([theme.fg("dim", "No context available")]), invalidate() {} }
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
const sections = raw.split(/---\n/).filter(Boolean)
|
|
97
|
-
const
|
|
101
|
+
const lineCount = raw.split("\n").length
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
+
// Collapsed: one-line summary
|
|
104
|
+
if (!opts.expanded) {
|
|
105
|
+
const summary = theme.fg("success", "✓ ") + theme.fg("muted", `${sections.length} items, ${lineCount} lines loaded (Ctrl+O to expand)`)
|
|
106
|
+
return { render: () => safeRender([summary]), invalidate() {} }
|
|
107
|
+
}
|
|
103
108
|
|
|
109
|
+
// Expanded: show sections with truncation
|
|
110
|
+
const lines: string[] = []
|
|
111
|
+
for (const section of sections) {
|
|
112
|
+
const firstLine = section.trim().split("\n")[0] ?? ""
|
|
104
113
|
if (firstLine.startsWith("[")) {
|
|
105
114
|
const typeMatch = firstLine.match(/^\[(\w+)\]\s*(.*)/)
|
|
106
115
|
if (typeMatch) {
|
|
107
116
|
const typeColor = typeMatch[1] === "decision" ? "warning" : typeMatch[1] === "feature" ? "success" : "muted"
|
|
108
|
-
lines.push(truncLine(`${theme.fg(typeColor, `[${typeMatch[1]}]`)} ${theme.fg("text", typeMatch[2])}
|
|
117
|
+
lines.push(truncLine(`${theme.fg(typeColor, `[${typeMatch[1]}]`)} ${theme.fg("text", typeMatch[2])}`))
|
|
109
118
|
} else {
|
|
110
|
-
lines.push(truncLine(theme.fg("text", firstLine)
|
|
119
|
+
lines.push(truncLine(theme.fg("text", firstLine)))
|
|
111
120
|
}
|
|
112
121
|
} else {
|
|
113
|
-
lines.push(truncLine(theme.fg("text", firstLine)
|
|
122
|
+
lines.push(truncLine(theme.fg("text", firstLine)))
|
|
114
123
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const l of rest) lines.push(truncLine(theme.fg("muted", l), MAX_W))
|
|
119
|
-
lines.push("")
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!opts.expanded && sections.length > 3) {
|
|
124
|
-
lines.push(theme.fg("dim", `... ${sections.length - 3} more results (Ctrl+O)`))
|
|
124
|
+
const rest = section.trim().split("\n").slice(1)
|
|
125
|
+
for (const l of rest) lines.push(truncLine(theme.fg("muted", l)))
|
|
126
|
+
lines.push("")
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
return { render: () => lines, invalidate() {} }
|
|
129
|
+
return { render: () => safeRender(lines), invalidate() {} }
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
// ─── CRM Tool ───────────────────────────────────────────────────────────────
|
|
@@ -133,13 +135,13 @@ export function crmRenderCall(args: Record<string, any>, theme: PiTheme): any {
|
|
|
133
135
|
const cmd = args.command ?? "list"
|
|
134
136
|
const extra = args.args ? ` ${args.args}` : ""
|
|
135
137
|
const label = theme.fg("toolTitle", theme.bold("crm ")) + theme.fg("accent", cmd) + theme.fg("dim", extra)
|
|
136
|
-
return { render: () => [label], invalidate() {} }
|
|
138
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export function crmRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
140
142
|
const raw = extractText(result)
|
|
141
143
|
if (raw.startsWith("Error")) {
|
|
142
|
-
return { render: () => [theme.fg("error", raw)], invalidate() {} }
|
|
144
|
+
return { render: () => safeRender([theme.fg("error", raw)]), invalidate() {} }
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
const lines = raw.split("\n").filter(Boolean)
|
|
@@ -154,10 +156,10 @@ export function crmRenderResult(result: any, opts: { expanded: boolean }, theme:
|
|
|
154
156
|
if (!opts.expanded && themed.length > 8) {
|
|
155
157
|
const display = themed.slice(0, 8)
|
|
156
158
|
display.push(theme.fg("dim", `... ${themed.length - 8} more (Ctrl+O)`))
|
|
157
|
-
return { render: () => display, invalidate() {} }
|
|
159
|
+
return { render: () => safeRender(display), invalidate() {} }
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
return { render: () => themed, invalidate() {} }
|
|
162
|
+
return { render: () => safeRender(themed), invalidate() {} }
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
// ─── Memory Search Tool ─────────────────────────────────────────────────────
|
|
@@ -166,36 +168,33 @@ export function memoryRenderCall(args: Record<string, any>, theme: PiTheme): any
|
|
|
166
168
|
const query = args.query ?? ""
|
|
167
169
|
const type = args.type && args.type !== "all" ? ` [${args.type}]` : ""
|
|
168
170
|
const label = theme.fg("toolTitle", theme.bold("memory ")) + theme.fg("accent", `"${query}"`) + theme.fg("muted", type)
|
|
169
|
-
return { render: () => [label], invalidate() {} }
|
|
171
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
export function memoryRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
173
175
|
const raw = extractText(result)
|
|
174
176
|
if (raw.includes("unavailable") || raw.includes("No memories")) {
|
|
175
|
-
return { render: () => [theme.fg("dim",
|
|
177
|
+
return { render: () => safeRender([theme.fg("dim", "No memories found")]), invalidate() {} }
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
const entries = raw.split(/\n\n---\n\n/).filter(Boolean)
|
|
179
|
-
const lines: string[] = []
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
if (header) lines.push(theme.fg("accent", header))
|
|
186
|
-
if (opts.expanded) {
|
|
187
|
-
for (const l of body) lines.push(theme.fg("muted", l))
|
|
188
|
-
lines.push("")
|
|
189
|
-
} else if (body.length > 0) {
|
|
190
|
-
lines.push(theme.fg("dim", body[0].slice(0, 80) + (body[0].length > 80 ? "…" : "")))
|
|
191
|
-
}
|
|
182
|
+
// Collapsed: one-line summary
|
|
183
|
+
if (!opts.expanded) {
|
|
184
|
+
const summary = theme.fg("success", "✓ ") + theme.fg("muted", `${entries.length} memories found (Ctrl+O to expand)`)
|
|
185
|
+
return { render: () => safeRender([summary]), invalidate() {} }
|
|
192
186
|
}
|
|
193
187
|
|
|
194
|
-
|
|
195
|
-
|
|
188
|
+
// Expanded: show entries
|
|
189
|
+
const lines: string[] = []
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const [header, ...body] = entry.trim().split("\n")
|
|
192
|
+
if (header) lines.push(truncLine(theme.fg("accent", header)))
|
|
193
|
+
for (const l of body) lines.push(truncLine(theme.fg("muted", l)))
|
|
194
|
+
lines.push("")
|
|
196
195
|
}
|
|
197
196
|
|
|
198
|
-
return { render: () => lines, invalidate() {} }
|
|
197
|
+
return { render: () => safeRender(lines), invalidate() {} }
|
|
199
198
|
}
|
|
200
199
|
|
|
201
200
|
// ─── Synopsis Tool ──────────────────────────────────────────────────────────
|
|
@@ -204,7 +203,7 @@ export function synopsisRenderCall(args: Record<string, any>, theme: PiTheme): a
|
|
|
204
203
|
const hours = args.hours ?? 24
|
|
205
204
|
const author = args.author ? ` by ${args.author}` : ""
|
|
206
205
|
const label = theme.fg("toolTitle", theme.bold("synopsis ")) + theme.fg("accent", `${hours}h`) + theme.fg("muted", author)
|
|
207
|
-
return { render: () => [label], invalidate() {} }
|
|
206
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
208
207
|
}
|
|
209
208
|
|
|
210
209
|
export function synopsisRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -225,17 +224,17 @@ export function synopsisRenderResult(result: any, opts: { expanded: boolean }, t
|
|
|
225
224
|
if (!opts.expanded && themed.length > 15) {
|
|
226
225
|
const display = themed.slice(0, 15)
|
|
227
226
|
display.push(theme.fg("dim", `... ${themed.length - 15} more lines (Ctrl+O)`))
|
|
228
|
-
return { render: () => display, invalidate() {} }
|
|
227
|
+
return { render: () => safeRender(display), invalidate() {} }
|
|
229
228
|
}
|
|
230
229
|
|
|
231
|
-
return { render: () => themed, invalidate() {} }
|
|
230
|
+
return { render: () => safeRender(themed), invalidate() {} }
|
|
232
231
|
}
|
|
233
232
|
|
|
234
233
|
// ─── Eval Status Tool ───────────────────────────────────────────────────────
|
|
235
234
|
|
|
236
235
|
export function evalStatusRenderCall(args: Record<string, any>, theme: PiTheme): any {
|
|
237
236
|
const label = theme.fg("toolTitle", theme.bold("eval")) + " " + theme.fg("accent", "status")
|
|
238
|
-
return { render: () => [label], invalidate() {} }
|
|
237
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
239
238
|
}
|
|
240
239
|
|
|
241
240
|
export function evalStatusRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -253,7 +252,7 @@ export function evalStatusRenderResult(result: any, opts: { expanded: boolean },
|
|
|
253
252
|
return theme.fg("toolOutput", line)
|
|
254
253
|
})
|
|
255
254
|
|
|
256
|
-
return { render: () => themed, invalidate() {} }
|
|
255
|
+
return { render: () => safeRender(themed), invalidate() {} }
|
|
257
256
|
}
|
|
258
257
|
|
|
259
258
|
// ─── Eval Compare Tool ──────────────────────────────────────────────────────
|
|
@@ -262,7 +261,7 @@ export function evalCompareRenderCall(args: Record<string, any>, theme: PiTheme)
|
|
|
262
261
|
const a = args.a ?? "-2"
|
|
263
262
|
const b = args.b ?? "-1"
|
|
264
263
|
const label = theme.fg("toolTitle", theme.bold("eval")) + " " + theme.fg("accent", `compare ${a} ↔ ${b}`)
|
|
265
|
-
return { render: () => [label], invalidate() {} }
|
|
264
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
266
265
|
}
|
|
267
266
|
|
|
268
267
|
export function evalCompareRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -273,7 +272,7 @@ export function evalCompareRenderResult(result: any, _opts: { expanded: boolean
|
|
|
273
272
|
if (/unchanged|same/i.test(line)) return theme.fg("dim", line)
|
|
274
273
|
return theme.fg("toolOutput", line)
|
|
275
274
|
})
|
|
276
|
-
return { render: () => lines, invalidate() {} }
|
|
275
|
+
return { render: () => safeRender(lines), invalidate() {} }
|
|
277
276
|
}
|
|
278
277
|
|
|
279
278
|
// ─── Policy Score Tool ──────────────────────────────────────────────────────
|
|
@@ -282,7 +281,7 @@ export function policyScoreRenderCall(args: Record<string, any>, theme: PiTheme)
|
|
|
282
281
|
const action = args.action_type ?? "?"
|
|
283
282
|
const scope = args.scope ? `[${args.scope}]` : ""
|
|
284
283
|
const label = theme.fg("toolTitle", theme.bold("policy ")) + theme.fg("accent", action) + " " + theme.fg("dim", scope)
|
|
285
|
-
return { render: () => [label], invalidate() {} }
|
|
284
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
286
285
|
}
|
|
287
286
|
|
|
288
287
|
export function policyScoreRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -293,7 +292,7 @@ export function policyScoreRenderResult(result: any, _opts: { expanded: boolean
|
|
|
293
292
|
if (/delta|score|predict/i.test(line)) return theme.fg("accent", line)
|
|
294
293
|
return theme.fg("toolOutput", line)
|
|
295
294
|
})
|
|
296
|
-
return { render: () => lines, invalidate() {} }
|
|
295
|
+
return { render: () => safeRender(lines), invalidate() {} }
|
|
297
296
|
}
|
|
298
297
|
|
|
299
298
|
// ─── Policy Rank Tool ───────────────────────────────────────────────────────
|
|
@@ -302,7 +301,7 @@ export function policyRankRenderCall(args: Record<string, any>, theme: PiTheme):
|
|
|
302
301
|
let count = 0
|
|
303
302
|
try { count = JSON.parse(args.actions ?? "[]").length } catch {}
|
|
304
303
|
const label = theme.fg("toolTitle", theme.bold("policy ")) + theme.fg("accent", `rank ${count} actions`)
|
|
305
|
-
return { render: () => [label], invalidate() {} }
|
|
304
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
export function policyRankRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -313,7 +312,7 @@ export function policyRankRenderResult(result: any, _opts: { expanded: boolean }
|
|
|
313
312
|
if (/delta|score/i.test(line)) return `${medal} ${theme.fg("accent", line)}`
|
|
314
313
|
return `${medal} ${theme.fg("toolOutput", line)}`
|
|
315
314
|
})
|
|
316
|
-
return { render: () => themed, invalidate() {} }
|
|
315
|
+
return { render: () => safeRender(themed), invalidate() {} }
|
|
317
316
|
}
|
|
318
317
|
|
|
319
318
|
// ─── Training Buffer Tool ───────────────────────────────────────────────────
|
|
@@ -323,7 +322,7 @@ export function trainingBufferRenderCall(args: Record<string, any>, theme: PiThe
|
|
|
323
322
|
const outcome = args.outcome ?? ""
|
|
324
323
|
const icon = outcome === "improved" ? theme.fg("success", "↑") : outcome === "regressed" ? theme.fg("error", "↓") : theme.fg("dim", "→")
|
|
325
324
|
const label = theme.fg("toolTitle", theme.bold("train ")) + `${icon} ${theme.fg("accent", action)}`
|
|
326
|
-
return { render: () => [label], invalidate() {} }
|
|
325
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
export function trainingBufferRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -331,7 +330,7 @@ export function trainingBufferRenderResult(result: any, _opts: { expanded: boole
|
|
|
331
330
|
const line = raw.includes("recorded") || raw.includes("saved")
|
|
332
331
|
? theme.fg("success", "✓ ") + theme.fg("muted", raw)
|
|
333
332
|
: theme.fg("toolOutput", raw)
|
|
334
|
-
return { render: () => [line], invalidate() {} }
|
|
333
|
+
return { render: () => safeRender([line]), invalidate() {} }
|
|
335
334
|
}
|
|
336
335
|
|
|
337
336
|
// ─── Mine Tuples Tool ───────────────────────────────────────────────────────
|
|
@@ -340,7 +339,7 @@ export function mineTuplesRenderCall(args: Record<string, any>, theme: PiTheme):
|
|
|
340
339
|
const source = args.source ?? "all"
|
|
341
340
|
const write = args.write === "yes" ? theme.fg("warning", " (write)") : ""
|
|
342
341
|
const label = theme.fg("toolTitle", theme.bold("mine ")) + theme.fg("accent", source) + write
|
|
343
|
-
return { render: () => [label], invalidate() {} }
|
|
342
|
+
return { render: () => safeRender([label]), invalidate() {} }
|
|
344
343
|
}
|
|
345
344
|
|
|
346
345
|
export function mineTuplesRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
|
|
@@ -350,7 +349,7 @@ export function mineTuplesRenderResult(result: any, _opts: { expanded: boolean }
|
|
|
350
349
|
if (/error|failed/i.test(line)) return theme.fg("error", line)
|
|
351
350
|
return theme.fg("toolOutput", line)
|
|
352
351
|
})
|
|
353
|
-
return { render: () => lines, invalidate() {} }
|
|
352
|
+
return { render: () => safeRender(lines), invalidate() {} }
|
|
354
353
|
}
|
|
355
354
|
|
|
356
355
|
// ─── Extract text helper ────────────────────────────────────────────────────
|