@pi-unipi/unipi 2.0.0 → 2.0.2
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/README.md +3 -3
- package/package.json +1 -1
- package/packages/ask-user/ask-ui.ts +61 -22
- package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +120 -0
- package/packages/autocomplete/src/constants.ts +26 -3
- package/packages/btw/README.md +17 -17
- package/packages/btw/extensions/btw.ts +29 -20
- package/packages/btw/skills/btw/SKILL.md +27 -27
- package/packages/cocoindex/bridge.ts +75 -7
- package/packages/compactor/src/commands/index.ts +12 -4
- package/packages/compactor/src/index.ts +14 -5
- package/packages/compactor/src/session/recall-blocks.ts +112 -0
- package/packages/compactor/src/tools/register.ts +8 -3
- package/packages/core/constants.ts +12 -0
package/README.md
CHANGED
|
@@ -77,12 +77,12 @@ Coexists triggers enhance behavior when packages are installed together. Workflo
|
|
|
77
77
|
| Workflow | `/unipi:` | brainstorm, plan, work, review-work, consolidate, quick-work, debug, fix |
|
|
78
78
|
| Ralph | `/unipi:ralph` | start, stop, resume, status |
|
|
79
79
|
| Memory | `/unipi:memory-` | process, search, consolidate, forget |
|
|
80
|
-
| Compactor | `/unipi:` | lossless-compact, compact-stats, compact-settings, compact-preset,
|
|
80
|
+
| Compactor | `/unipi:` | lossless-compact, session-recall, compact-stats, compact-settings, compact-preset, compact-help |
|
|
81
81
|
| Notify | `/unipi:notify-` | settings, test, set-tg, set-ntfy |
|
|
82
82
|
| MCP | `/unipi:mcp-` | add, settings, sync, status |
|
|
83
83
|
| Web | `/unipi:web-` | settings, cache-clear |
|
|
84
|
-
| BTW | `/btw` | question, new, tangent, inject, summarize |
|
|
85
|
-
| Utility | `/unipi:` | env, doctor, status, cleanup, name
|
|
84
|
+
| BTW | `/unipi:btw` | question, btw-new, btw-tangent, btw-inject, btw-summarize |
|
|
85
|
+
| Utility | `/unipi:` | env, doctor, status, cleanup, badge-name |
|
|
86
86
|
| Milestone | `/unipi:milestone-` | onboard, update |
|
|
87
87
|
| Kanboard | `/unipi:kanboard` | toggle, doctor |
|
|
88
88
|
| Footer | `/unipi:footer` | toggle, settings |
|
package/package.json
CHANGED
|
@@ -450,6 +450,20 @@ export function renderAskUI(params: {
|
|
|
450
450
|
theme: Theme,
|
|
451
451
|
width: number,
|
|
452
452
|
) {
|
|
453
|
+
const addWrappedOptionLine = (prefix: string, content: string) => {
|
|
454
|
+
const prefixWidth = visibleWidth(prefix);
|
|
455
|
+
const contentWidth = Math.max(1, width - prefixWidth);
|
|
456
|
+
const continuationPrefix = " ".repeat(prefixWidth);
|
|
457
|
+
const wrapped = wrapTextWithAnsi(content, contentWidth);
|
|
458
|
+
for (let lineIndex = 0; lineIndex < wrapped.length; lineIndex++) {
|
|
459
|
+
add((lineIndex === 0 ? prefix : continuationPrefix) + wrapped[lineIndex]);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const addWrappedDescription = (description: string) => {
|
|
464
|
+
addWrappedOptionLine(" ", theme.fg("muted", description));
|
|
465
|
+
};
|
|
466
|
+
|
|
453
467
|
for (let i = 0; i < displayOptions.length; i++) {
|
|
454
468
|
const opt = displayOptions[i];
|
|
455
469
|
const isSelected = i === optionIndex;
|
|
@@ -467,11 +481,9 @@ export function renderAskUI(params: {
|
|
|
467
481
|
label = `${opt.label}: "${customText}"`;
|
|
468
482
|
}
|
|
469
483
|
|
|
470
|
-
|
|
471
|
-
prefix +
|
|
472
|
-
|
|
473
|
-
" " +
|
|
474
|
-
theme.fg(isSelected ? "accent" : "text", label),
|
|
484
|
+
addWrappedOptionLine(
|
|
485
|
+
prefix + theme.fg(color, `[${box}]`) + " ",
|
|
486
|
+
theme.fg(isSelected ? "accent" : "text", label),
|
|
475
487
|
);
|
|
476
488
|
|
|
477
489
|
// Show edit indicator if in edit mode for this option
|
|
@@ -493,11 +505,9 @@ export function renderAskUI(params: {
|
|
|
493
505
|
label = `${opt.label}: "${optCustom}"`;
|
|
494
506
|
}
|
|
495
507
|
|
|
496
|
-
|
|
497
|
-
prefix +
|
|
498
|
-
|
|
499
|
-
" " +
|
|
500
|
-
theme.fg(isSelected ? "accent" : "text", label),
|
|
508
|
+
addWrappedOptionLine(
|
|
509
|
+
prefix + theme.fg(color, `[${box}]`) + " ",
|
|
510
|
+
theme.fg(isSelected ? "accent" : "text", label),
|
|
501
511
|
);
|
|
502
512
|
|
|
503
513
|
// Show edit indicator if in edit mode for this option
|
|
@@ -525,11 +535,11 @@ export function renderAskUI(params: {
|
|
|
525
535
|
label += theme.fg("dim", " ↗");
|
|
526
536
|
}
|
|
527
537
|
|
|
528
|
-
|
|
529
|
-
prefix
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
538
|
+
addWrappedOptionLine(
|
|
539
|
+
prefix,
|
|
540
|
+
isSelected
|
|
541
|
+
? theme.fg("accent", label)
|
|
542
|
+
: theme.fg("text", label),
|
|
533
543
|
);
|
|
534
544
|
|
|
535
545
|
// Show edit indicator if in edit mode for this option
|
|
@@ -543,7 +553,7 @@ export function renderAskUI(params: {
|
|
|
543
553
|
|
|
544
554
|
// Description
|
|
545
555
|
if (opt.description) {
|
|
546
|
-
|
|
556
|
+
addWrappedDescription(opt.description);
|
|
547
557
|
}
|
|
548
558
|
}
|
|
549
559
|
}
|
|
@@ -564,17 +574,46 @@ export function renderAskUI(params: {
|
|
|
564
574
|
export function createRenderCall() {
|
|
565
575
|
return (args: Record<string, unknown>, theme: Theme, _context: unknown) => {
|
|
566
576
|
const question = (args.question as string) || "";
|
|
567
|
-
const
|
|
577
|
+
const context = (args.context as string | undefined) || "";
|
|
578
|
+
const options = Array.isArray(args.options)
|
|
579
|
+
? (args.options as Array<Record<string, unknown>>)
|
|
580
|
+
: [];
|
|
568
581
|
const mode = args.allowMultiple ? "multi-select" : "single-select";
|
|
582
|
+
const allowFreeform = args.allowFreeform !== false;
|
|
569
583
|
const count = options.length;
|
|
570
584
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
theme.fg("
|
|
585
|
+
const lines: string[] = [];
|
|
586
|
+
lines.push(
|
|
587
|
+
theme.fg("toolTitle", theme.bold("ask_user")) +
|
|
588
|
+
theme.fg("dim", ` (${count} option${count !== 1 ? "s" : ""}, ${mode}${allowFreeform ? ", freeform" : ""})`),
|
|
589
|
+
);
|
|
590
|
+
if (context) {
|
|
591
|
+
lines.push(theme.fg("muted", "Context: ") + theme.fg("text", context));
|
|
592
|
+
}
|
|
593
|
+
lines.push(theme.fg("muted", "Question: ") + theme.fg("text", question));
|
|
594
|
+
|
|
574
595
|
if (count > 0) {
|
|
575
|
-
|
|
596
|
+
lines.push(theme.fg("muted", "Options:"));
|
|
597
|
+
options.forEach((option, index) => {
|
|
598
|
+
const label = String(option.label ?? option.value ?? `Option ${index + 1}`);
|
|
599
|
+
const action = typeof option.action === "string" && option.action !== "select"
|
|
600
|
+
? theme.fg("dim", ` [${option.action}]`)
|
|
601
|
+
: "";
|
|
602
|
+
lines.push(
|
|
603
|
+
theme.fg("dim", ` ${index + 1}. `) +
|
|
604
|
+
theme.fg("text", label) +
|
|
605
|
+
action,
|
|
606
|
+
);
|
|
607
|
+
if (typeof option.description === "string" && option.description.trim()) {
|
|
608
|
+
lines.push(theme.fg("muted", ` ${option.description}`));
|
|
609
|
+
}
|
|
610
|
+
if (typeof option.prefill === "string" && option.prefill.trim()) {
|
|
611
|
+
lines.push(theme.fg("dim", ` prefill: ${option.prefill}`));
|
|
612
|
+
}
|
|
613
|
+
});
|
|
576
614
|
}
|
|
577
|
-
|
|
615
|
+
|
|
616
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
578
617
|
};
|
|
579
618
|
}
|
|
580
619
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { existsSync, globSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function findRepoRoot(start: string): string {
|
|
6
|
+
let dir = start;
|
|
7
|
+
while (dir !== dirname(dir)) {
|
|
8
|
+
if (existsSync(join(dir, "packages", "autocomplete", "src", "constants.ts"))) return dir;
|
|
9
|
+
dir = dirname(dir);
|
|
10
|
+
}
|
|
11
|
+
return start;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const root = findRepoRoot(process.cwd());
|
|
15
|
+
|
|
16
|
+
function read(path: string): string {
|
|
17
|
+
return readFileSync(join(root, path), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectConstants(): Map<string, string> {
|
|
21
|
+
const constants = new Map<string, string>([["UNIPI_PREFIX", "unipi:"]]);
|
|
22
|
+
|
|
23
|
+
for (const path of globSync("packages/**/*.ts", { cwd: root })) {
|
|
24
|
+
const text = read(path);
|
|
25
|
+
for (const obj of text.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*\{([\s\S]*?)\}\s*as\s+const/g)) {
|
|
26
|
+
const [, name, body] = obj;
|
|
27
|
+
for (const item of body.matchAll(/(\w+):\s*"([^"]+)"/g)) {
|
|
28
|
+
constants.set(`${name}.${item[1]}`, item[2]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return constants;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function evaluateCommandExpression(expr: string, constants: Map<string, string>): string | null {
|
|
37
|
+
const trimmed = expr.trim();
|
|
38
|
+
|
|
39
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
40
|
+
const quote = trimmed[0];
|
|
41
|
+
const end = trimmed.indexOf(quote, 1);
|
|
42
|
+
return end > 0 ? trimmed.slice(1, end) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (trimmed.startsWith("`")) {
|
|
46
|
+
const end = trimmed.lastIndexOf("`");
|
|
47
|
+
if (end <= 0) return null;
|
|
48
|
+
return trimmed.slice(1, end).replace(/\$\{([^}]+)\}/g, (_match, key: string) => {
|
|
49
|
+
const resolved = constants.get(key.trim());
|
|
50
|
+
return resolved ?? `\${${key}}`;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return constants.get(trimmed) ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function registeredCommands(): { commands: Set<string>; nonUnipi: string[]; unresolved: string[] } {
|
|
58
|
+
const constants = collectConstants();
|
|
59
|
+
const commands = new Set<string>();
|
|
60
|
+
const nonUnipi: string[] = [];
|
|
61
|
+
const unresolved: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const path of globSync("packages/**/*.ts", { cwd: root }).sort()) {
|
|
64
|
+
const text = read(path);
|
|
65
|
+
if (!text.includes("registerCommand")) continue;
|
|
66
|
+
|
|
67
|
+
for (const match of text.matchAll(/\.registerCommand\(\s*([^,\n]+)/g)) {
|
|
68
|
+
const expr = match[1].trim();
|
|
69
|
+
|
|
70
|
+
// Workflow registers a loop over WORKFLOW_COMMANDS via local `fullCommand`.
|
|
71
|
+
if (path === "packages/workflow/commands.ts" && expr === "fullCommand") {
|
|
72
|
+
for (const [key, value] of constants) {
|
|
73
|
+
if (key.startsWith("WORKFLOW_COMMANDS.")) commands.add(`unipi:${value}`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const command = evaluateCommandExpression(expr, constants);
|
|
79
|
+
if (!command || command.includes("${")) {
|
|
80
|
+
unresolved.push(`${path}: ${expr}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (command.startsWith("unipi:")) {
|
|
85
|
+
commands.add(command);
|
|
86
|
+
} else {
|
|
87
|
+
nonUnipi.push(`${command} (${path})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { commands, nonUnipi, unresolved };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function autocompleteRegistry(): { registry: Set<string>; descriptions: Set<string>; registryPackages: Set<string>; labels: Set<string> } {
|
|
96
|
+
const text = read("packages/autocomplete/src/constants.ts");
|
|
97
|
+
const registryBody = text.match(/export const COMMAND_REGISTRY[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
|
|
98
|
+
const descriptionsBody = text.match(/export const COMMAND_DESCRIPTIONS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
|
|
99
|
+
const labelsBody = text.match(/export const PACKAGE_LABELS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
|
|
100
|
+
|
|
101
|
+
const registry = new Set([...registryBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
|
|
102
|
+
const descriptions = new Set([...descriptionsBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
|
|
103
|
+
const registryPackages = new Set([...registryBody.matchAll(/"unipi:[^"]+"\s*:\s*"([^"]+)"/g)].map((m) => m[1]));
|
|
104
|
+
const labels = new Set([...labelsBody.matchAll(/^\s*"?([a-z][a-z0-9-]*)"?:\s*"/gm)].map((m) => m[1]));
|
|
105
|
+
|
|
106
|
+
return { registry, descriptions, registryPackages, labels };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe("autocomplete command registry audit", () => {
|
|
110
|
+
it("mirrors every registered /unipi:* command and has no non-unipi package commands", () => {
|
|
111
|
+
const registered = registeredCommands();
|
|
112
|
+
const autocomplete = autocompleteRegistry();
|
|
113
|
+
|
|
114
|
+
expect(registered.unresolved, "unresolved registerCommand expressions").toEqual([]);
|
|
115
|
+
expect(registered.nonUnipi, "package commands must use the unipi: prefix").toEqual([]);
|
|
116
|
+
expect([...registered.commands].sort()).toEqual([...autocomplete.registry].sort());
|
|
117
|
+
expect([...autocomplete.registry].sort()).toEqual([...autocomplete.descriptions].sort());
|
|
118
|
+
expect([...autocomplete.registryPackages].filter((pkg) => !autocomplete.labels.has(pkg)).sort()).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -20,6 +20,7 @@ export const PACKAGE_ORDER: string[] = [
|
|
|
20
20
|
"workflow",
|
|
21
21
|
"ralph",
|
|
22
22
|
"memory",
|
|
23
|
+
"btw",
|
|
23
24
|
"milestone",
|
|
24
25
|
"mcp",
|
|
25
26
|
"utility",
|
|
@@ -41,6 +42,7 @@ export const PACKAGE_COLORS: Record<string, string> = {
|
|
|
41
42
|
workflow: `${ESC}[91m`, // Bright Red
|
|
42
43
|
ralph: `${ESC}[33m`, // Yellow/Orange
|
|
43
44
|
memory: `${ESC}[93m`, // Bright Yellow
|
|
45
|
+
btw: `${ESC}[95m`, // Bright Magenta
|
|
44
46
|
milestone: `${ESC}[32m`, // Green
|
|
45
47
|
mcp: `${ESC}[32m`, // Green
|
|
46
48
|
utility: `${ESC}[36m`, // Cyan
|
|
@@ -57,7 +59,7 @@ export const PACKAGE_COLORS: Record<string, string> = {
|
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
// ─── Command Registry ────────────────────────────────────────────────
|
|
60
|
-
/** Mapping of full command name → package name (
|
|
62
|
+
/** Mapping of full command name → package name (88 verified commands) */
|
|
61
63
|
export const COMMAND_REGISTRY: Record<string, string> = {
|
|
62
64
|
// workflow (20 commands)
|
|
63
65
|
"unipi:brainstorm": "workflow",
|
|
@@ -95,6 +97,14 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
95
97
|
"unipi:global-memory-list": "memory",
|
|
96
98
|
"unipi:memory-settings": "memory",
|
|
97
99
|
|
|
100
|
+
// btw (6 commands)
|
|
101
|
+
"unipi:btw": "btw",
|
|
102
|
+
"unipi:btw-tangent": "btw",
|
|
103
|
+
"unipi:btw-new": "btw",
|
|
104
|
+
"unipi:btw-clear": "btw",
|
|
105
|
+
"unipi:btw-inject": "btw",
|
|
106
|
+
"unipi:btw-summarize": "btw",
|
|
107
|
+
|
|
98
108
|
// mcp (5 commands)
|
|
99
109
|
"unipi:mcp-status": "mcp",
|
|
100
110
|
"unipi:mcp-sync": "mcp",
|
|
@@ -126,14 +136,16 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
126
136
|
"unipi:web-settings": "web-api",
|
|
127
137
|
"unipi:web-cache-clear": "web-api",
|
|
128
138
|
|
|
129
|
-
// compact (
|
|
139
|
+
// compact (9 commands)
|
|
130
140
|
"unipi:lossless-compact": "compact",
|
|
131
141
|
"unipi:compact": "compact",
|
|
142
|
+
"unipi:session-recall": "compact",
|
|
132
143
|
"unipi:compact-recall": "compact",
|
|
133
144
|
"unipi:compact-stats": "compact",
|
|
134
145
|
"unipi:compact-doctor": "compact",
|
|
135
146
|
"unipi:compact-settings": "compact",
|
|
136
147
|
"unipi:compact-preset": "compact",
|
|
148
|
+
"unipi:compact-help": "compact",
|
|
137
149
|
|
|
138
150
|
// cocoindex (5 commands)
|
|
139
151
|
"unipi:cocoindex-update": "cocoindex",
|
|
@@ -208,6 +220,13 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
208
220
|
"unipi:global-memory-list": "List all project memories",
|
|
209
221
|
"unipi:memory-settings": "Configure memory settings",
|
|
210
222
|
|
|
223
|
+
"unipi:btw": "Run a parallel side conversation",
|
|
224
|
+
"unipi:btw-tangent": "Start a contextless BTW tangent thread",
|
|
225
|
+
"unipi:btw-new": "Start a fresh BTW thread with session context",
|
|
226
|
+
"unipi:btw-clear": "Dismiss and clear the BTW thread",
|
|
227
|
+
"unipi:btw-inject": "Inject the BTW thread into the main agent",
|
|
228
|
+
"unipi:btw-summarize": "Summarize and inject the BTW thread",
|
|
229
|
+
|
|
211
230
|
"unipi:mcp-status": "Show MCP server status",
|
|
212
231
|
"unipi:mcp-sync": "Sync MCP server connections",
|
|
213
232
|
"unipi:mcp-add": "Add a new MCP server",
|
|
@@ -238,11 +257,13 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
238
257
|
|
|
239
258
|
"unipi:lossless-compact": "Immediate zero-LLM compaction",
|
|
240
259
|
"unipi:compact": "(DEPRECATED) Use /unipi:lossless-compact instead",
|
|
241
|
-
"unipi:
|
|
260
|
+
"unipi:session-recall": "Search session history, including compacted-away messages",
|
|
261
|
+
"unipi:compact-recall": "(DEPRECATED) Use /unipi:session-recall instead",
|
|
242
262
|
"unipi:compact-stats": "Show compaction statistics",
|
|
243
263
|
"unipi:compact-doctor": "Diagnose compaction issues",
|
|
244
264
|
"unipi:compact-settings": "Configure compaction settings",
|
|
245
265
|
"unipi:compact-preset": "Manage compaction presets",
|
|
266
|
+
"unipi:compact-help": "Show compactor command help",
|
|
246
267
|
"unipi:cocoindex-update": "Run CocoIndex update to index project",
|
|
247
268
|
"unipi:cocoindex-status": "Show CocoIndex indexing status",
|
|
248
269
|
"unipi:cocoindex-init": "Initialize CocoIndex pipeline",
|
|
@@ -276,6 +297,7 @@ export const PACKAGE_LABELS: Record<string, string> = {
|
|
|
276
297
|
workflow: "workflow",
|
|
277
298
|
ralph: "ralph",
|
|
278
299
|
memory: "memory",
|
|
300
|
+
btw: "btw",
|
|
279
301
|
milestone: "milestone",
|
|
280
302
|
mcp: "mcp",
|
|
281
303
|
utility: "utility",
|
|
@@ -288,4 +310,5 @@ export const PACKAGE_LABELS: Record<string, string> = {
|
|
|
288
310
|
footer: "footer",
|
|
289
311
|
updater: "updater",
|
|
290
312
|
"input-shortcuts": "input-shortcuts",
|
|
313
|
+
cocoindex: "cocoindex",
|
|
291
314
|
};
|
package/packages/btw/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pi-unipi/btw
|
|
2
2
|
|
|
3
|
-
Side conversations that run in parallel. Ask a question using `/btw` while the main agent keeps working — the answer streams into a modal overlay without interrupting the current task.
|
|
3
|
+
Side conversations that run in parallel. Ask a question using `/unipi:btw` while the main agent keeps working — the answer streams into a modal overlay without interrupting the current task.
|
|
4
4
|
|
|
5
5
|
BTW opens a real Pi sub-session with coding-tool access. Use it to clarify something, explore an idea, or think through next steps without derailing the main turn. When you're ready, inject the thread back or summarize it.
|
|
6
6
|
|
|
@@ -10,12 +10,12 @@ Based on [pi-btw](https://github.com/Neuron-Mr-White/pi-btw) by Dan Bachelder.
|
|
|
10
10
|
|
|
11
11
|
| Command | Description |
|
|
12
12
|
|---------|-------------|
|
|
13
|
-
| `/btw [--save] <question>` | Ask a question in a side thread |
|
|
14
|
-
| `/btw
|
|
15
|
-
| `/btw
|
|
16
|
-
| `/btw
|
|
17
|
-
| `/btw
|
|
18
|
-
| `/btw
|
|
13
|
+
| `/unipi:btw [--save] <question>` | Ask a question in a side thread |
|
|
14
|
+
| `/unipi:btw-new [question]` | Start a fresh thread with main-session context |
|
|
15
|
+
| `/unipi:btw-tangent [--save] <question>` | Contextless tangent thread |
|
|
16
|
+
| `/unipi:btw-clear` | Dismiss modal and clear thread |
|
|
17
|
+
| `/unipi:btw-inject [instructions]` | Send full thread to main agent |
|
|
18
|
+
| `/unipi:btw-summarize [instructions]` | Summarize thread and inject into main agent |
|
|
19
19
|
|
|
20
20
|
### Keyboard Shortcuts
|
|
21
21
|
|
|
@@ -29,13 +29,13 @@ Based on [pi-btw](https://github.com/Neuron-Mr-White/pi-btw) by Dan Bachelder.
|
|
|
29
29
|
### Examples
|
|
30
30
|
|
|
31
31
|
```text
|
|
32
|
-
/btw what file defines this route?
|
|
33
|
-
/btw how would you refactor this parser?
|
|
34
|
-
/btw --save summarize the last error in one sentence
|
|
35
|
-
/btw
|
|
36
|
-
/btw
|
|
37
|
-
/btw
|
|
38
|
-
/btw
|
|
32
|
+
/unipi:btw what file defines this route?
|
|
33
|
+
/unipi:btw how would you refactor this parser?
|
|
34
|
+
/unipi:btw --save summarize the last error in one sentence
|
|
35
|
+
/unipi:btw-new let's start a fresh thread about auth
|
|
36
|
+
/unipi:btw-tangent brainstorm from first principles without using the current chat context
|
|
37
|
+
/unipi:btw-inject implement the plan we just discussed
|
|
38
|
+
/unipi:btw-summarize turn that side thread into a short handoff
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Special Triggers
|
|
@@ -46,14 +46,14 @@ The BTW overlay opens top-centered so the main session remains visible underneat
|
|
|
46
46
|
|
|
47
47
|
## How It Works
|
|
48
48
|
|
|
49
|
-
1. `/btw` creates or reuses a BTW sub-session
|
|
49
|
+
1. `/unipi:btw` creates or reuses a BTW sub-session
|
|
50
50
|
2. Your question runs in a real Pi session with tool access
|
|
51
51
|
3. The answer streams into the BTW modal overlay
|
|
52
52
|
4. The thread continues until you clear it or inject it back
|
|
53
53
|
|
|
54
|
-
`/btw
|
|
54
|
+
`/unipi:btw-inject` sends the full thread to the main agent as a user message. If Pi is busy, it queues as a follow-up. `/unipi:btw-summarize` does the same but summarizes first.
|
|
55
55
|
|
|
56
|
-
`/btw
|
|
56
|
+
`/unipi:btw-tangent` starts a separate thread that doesn't inherit the main session's conversation context. Use it for unrelated exploration.
|
|
57
57
|
|
|
58
58
|
The `--save` flag saves that single exchange as a visible session note.
|
|
59
59
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pi-unipi/btw — Side Conversation Extension
|
|
3
3
|
*
|
|
4
|
-
* A /btw side conversation channel that opens a real pi sub-session
|
|
4
|
+
* A /unipi:btw side conversation channel that opens a real pi sub-session
|
|
5
5
|
* with coding-tool access, running immediately even while the main
|
|
6
6
|
* agent is still busy.
|
|
7
7
|
*
|
|
8
8
|
* Based on pi-btw by Dan Bachelder, adapted for the Unipi suite.
|
|
9
9
|
*
|
|
10
10
|
* Commands:
|
|
11
|
-
* /btw [--save] <question>
|
|
12
|
-
* /btw
|
|
13
|
-
* /btw
|
|
14
|
-
* /btw
|
|
15
|
-
* /btw
|
|
16
|
-
* /btw
|
|
11
|
+
* /unipi:btw [--save] <question> - Side conversation (contextual)
|
|
12
|
+
* /unipi:btw-tangent [--save] <q> - Contextless tangent thread
|
|
13
|
+
* /unipi:btw-new [question] - Fresh thread with main-session context
|
|
14
|
+
* /unipi:btw-clear - Dismiss and clear thread
|
|
15
|
+
* /unipi:btw-inject [instructions] - Send full thread to main agent
|
|
16
|
+
* /unipi:btw-summarize [instructions] - Summarize and inject into main agent
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import {
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type OverlayHandle,
|
|
45
45
|
type TUI,
|
|
46
46
|
} from "@mariozechner/pi-tui";
|
|
47
|
+
import { BTW_COMMANDS, UNIPI_PREFIX } from "@pi-unipi/core";
|
|
47
48
|
|
|
48
49
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
49
50
|
|
|
@@ -1615,15 +1616,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1615
1616
|
|
|
1616
1617
|
function parseOverlayBtwCommand(value: string): { name: string; args: string } | null {
|
|
1617
1618
|
const trimmed = value.trim();
|
|
1618
|
-
const
|
|
1619
|
-
if (
|
|
1620
|
-
return
|
|
1619
|
+
const legacy = trimmed.match(/^\/(btw:(?:new|tangent|clear|inject|summarize))(?:\s+(.*))?$/);
|
|
1620
|
+
if (legacy) {
|
|
1621
|
+
return {
|
|
1622
|
+
name: legacy[1],
|
|
1623
|
+
args: legacy[2]?.trim() ?? "",
|
|
1624
|
+
};
|
|
1621
1625
|
}
|
|
1622
1626
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
+
const unipi = trimmed.match(/^\/unipi:btw-(new|tangent|clear|inject|summarize)(?:\s+(.*))?$/);
|
|
1628
|
+
if (unipi) {
|
|
1629
|
+
return {
|
|
1630
|
+
name: `btw:${unipi[1]}`,
|
|
1631
|
+
args: unipi[2]?.trim() ?? "",
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
return null;
|
|
1627
1636
|
}
|
|
1628
1637
|
|
|
1629
1638
|
async function submitFromOverlay(ctx: ExtensionCommandContext | ExtensionContext, value: string): Promise<void> {
|
|
@@ -1928,42 +1937,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
1928
1937
|
});
|
|
1929
1938
|
}
|
|
1930
1939
|
|
|
1931
|
-
pi.registerCommand(
|
|
1940
|
+
pi.registerCommand(`${UNIPI_PREFIX}${BTW_COMMANDS.BTW}`, {
|
|
1932
1941
|
description: "Continue a side conversation in a focused BTW modal. Add --save to also persist a visible note.",
|
|
1933
1942
|
handler: async (args, ctx) => {
|
|
1934
1943
|
await dispatchBtwCommand("btw", args, ctx);
|
|
1935
1944
|
},
|
|
1936
1945
|
});
|
|
1937
1946
|
|
|
1938
|
-
pi.registerCommand(
|
|
1947
|
+
pi.registerCommand(`${UNIPI_PREFIX}${BTW_COMMANDS.TANGENT}`, {
|
|
1939
1948
|
description: "Start or continue a contextless BTW tangent in the focused BTW modal.",
|
|
1940
1949
|
handler: async (args, ctx) => {
|
|
1941
1950
|
await dispatchBtwCommand("btw:tangent", args, ctx);
|
|
1942
1951
|
},
|
|
1943
1952
|
});
|
|
1944
1953
|
|
|
1945
|
-
pi.registerCommand(
|
|
1954
|
+
pi.registerCommand(`${UNIPI_PREFIX}${BTW_COMMANDS.NEW}`, {
|
|
1946
1955
|
description: "Start a fresh BTW thread with main-session context. Optionally ask the first question immediately.",
|
|
1947
1956
|
handler: async (args, ctx) => {
|
|
1948
1957
|
await dispatchBtwCommand("btw:new", args, ctx);
|
|
1949
1958
|
},
|
|
1950
1959
|
});
|
|
1951
1960
|
|
|
1952
|
-
pi.registerCommand(
|
|
1961
|
+
pi.registerCommand(`${UNIPI_PREFIX}${BTW_COMMANDS.CLEAR}`, {
|
|
1953
1962
|
description: "Dismiss the BTW modal/widget and clear the current thread.",
|
|
1954
1963
|
handler: async (args, ctx) => {
|
|
1955
1964
|
await dispatchBtwCommand("btw:clear", args, ctx);
|
|
1956
1965
|
},
|
|
1957
1966
|
});
|
|
1958
1967
|
|
|
1959
|
-
pi.registerCommand(
|
|
1968
|
+
pi.registerCommand(`${UNIPI_PREFIX}${BTW_COMMANDS.INJECT}`, {
|
|
1960
1969
|
description: "Inject the full BTW thread into the main agent as a user message.",
|
|
1961
1970
|
handler: async (args, ctx) => {
|
|
1962
1971
|
await dispatchBtwCommand("btw:inject", args, ctx);
|
|
1963
1972
|
},
|
|
1964
1973
|
});
|
|
1965
1974
|
|
|
1966
|
-
pi.registerCommand(
|
|
1975
|
+
pi.registerCommand(`${UNIPI_PREFIX}${BTW_COMMANDS.SUMMARIZE}`, {
|
|
1967
1976
|
description: "Summarize the BTW thread, then inject the summary into the main agent.",
|
|
1968
1977
|
handler: async (args, ctx) => {
|
|
1969
1978
|
await dispatchBtwCommand("btw:summarize", args, ctx);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: btw
|
|
3
|
-
description: Helps you use the /btw side-conversation workflow effectively. Use when you want to think in parallel, ask side questions without interrupting ongoing work, or inject a side thread back into the main agent.
|
|
3
|
+
description: Helps you use the /unipi:btw side-conversation workflow effectively. Use when you want to think in parallel, ask side questions without interrupting ongoing work, or inject a side thread back into the main agent.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# BTW
|
|
@@ -21,14 +21,14 @@ Prefer the BTW workflow when the user wants to:
|
|
|
21
21
|
Use these commands in your guidance to the user:
|
|
22
22
|
|
|
23
23
|
```text
|
|
24
|
-
/btw <question>
|
|
25
|
-
/btw --save <question>
|
|
26
|
-
/btw
|
|
27
|
-
/btw
|
|
28
|
-
/btw
|
|
29
|
-
/btw
|
|
30
|
-
/btw
|
|
31
|
-
/btw
|
|
24
|
+
/unipi:btw <question>
|
|
25
|
+
/unipi:btw --save <question>
|
|
26
|
+
/unipi:btw-new [question]
|
|
27
|
+
/unipi:btw-tangent <question>
|
|
28
|
+
/unipi:btw-tangent --save <question>
|
|
29
|
+
/unipi:btw-clear
|
|
30
|
+
/unipi:btw-inject [instructions]
|
|
31
|
+
/unipi:btw-summarize [instructions]
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## How to guide the user
|
|
@@ -38,7 +38,7 @@ Use these commands in your guidance to the user:
|
|
|
38
38
|
Recommend:
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
|
-
/btw <question>
|
|
41
|
+
/unipi:btw <question>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Use this when the user wants an immediate aside and does not need a visible saved note.
|
|
@@ -48,7 +48,7 @@ Use this when the user wants an immediate aside and does not need a visible save
|
|
|
48
48
|
Recommend:
|
|
49
49
|
|
|
50
50
|
```text
|
|
51
|
-
/btw --save <question>
|
|
51
|
+
/unipi:btw --save <question>
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Use this when the user wants the exchange to appear as a visible BTW note in the session transcript.
|
|
@@ -58,13 +58,13 @@ Use this when the user wants the exchange to appear as a visible BTW note in the
|
|
|
58
58
|
Recommend:
|
|
59
59
|
|
|
60
60
|
```text
|
|
61
|
-
/btw
|
|
61
|
+
/unipi:btw-new
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
or
|
|
65
65
|
|
|
66
66
|
```text
|
|
67
|
-
/btw
|
|
67
|
+
/unipi:btw-new <question>
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Use this when the previous BTW discussion is no longer relevant, but you still want the new side thread to inherit the current main-session context.
|
|
@@ -74,13 +74,13 @@ Use this when the previous BTW discussion is no longer relevant, but you still w
|
|
|
74
74
|
Recommend:
|
|
75
75
|
|
|
76
76
|
```text
|
|
77
|
-
/btw
|
|
77
|
+
/unipi:btw-tangent <question>
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
or
|
|
81
81
|
|
|
82
82
|
```text
|
|
83
|
-
/btw
|
|
83
|
+
/unipi:btw-tangent --save <question>
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Use this when the user wants a side conversation that does not include the current main-session context.
|
|
@@ -90,7 +90,7 @@ Use this when the user wants a side conversation that does not include the curre
|
|
|
90
90
|
Recommend:
|
|
91
91
|
|
|
92
92
|
```text
|
|
93
|
-
/btw
|
|
93
|
+
/unipi:btw-inject <instructions>
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
Use this when the exact discussion matters and the user wants the main agent to act on it.
|
|
@@ -100,19 +100,19 @@ Use this when the exact discussion matters and the user wants the main agent to
|
|
|
100
100
|
Recommend:
|
|
101
101
|
|
|
102
102
|
```text
|
|
103
|
-
/btw
|
|
103
|
+
/unipi:btw-summarize <instructions>
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Use this when the thread is long and only the distilled outcome should go back into the main agent.
|
|
107
107
|
|
|
108
108
|
## Recommendation rules
|
|
109
109
|
|
|
110
|
-
- Prefer `/btw` over normal chat when the user explicitly wants a side conversation.
|
|
111
|
-
- Prefer `/btw
|
|
112
|
-
- Prefer `/btw
|
|
113
|
-
- Prefer `/btw
|
|
114
|
-
- Suggest `/btw
|
|
115
|
-
- Suggest `/btw
|
|
110
|
+
- Prefer `/unipi:btw` over normal chat when the user explicitly wants a side conversation.
|
|
111
|
+
- Prefer `/unipi:btw-tangent` when the user wants that side conversation to be contextless.
|
|
112
|
+
- Prefer `/unipi:btw-summarize` over `/unipi:btw-inject` for long exploratory threads.
|
|
113
|
+
- Prefer `/unipi:btw-inject` when precise wording, detailed tradeoffs, or a full plan matters.
|
|
114
|
+
- Suggest `/unipi:btw-new` before starting a totally unrelated side topic when main-session context is still useful.
|
|
115
|
+
- Suggest `/unipi:btw-clear` when the widget/thread should be dismissed.
|
|
116
116
|
|
|
117
117
|
## Response style
|
|
118
118
|
|
|
@@ -127,23 +127,23 @@ When helping the user use BTW:
|
|
|
127
127
|
### Example: brainstorm while coding continues
|
|
128
128
|
|
|
129
129
|
```text
|
|
130
|
-
/btw what are the risks of switching this to optimistic updates?
|
|
130
|
+
/unipi:btw what are the risks of switching this to optimistic updates?
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
### Example: create a clean new thread
|
|
134
134
|
|
|
135
135
|
```text
|
|
136
|
-
/btw
|
|
136
|
+
/unipi:btw-new sketch a safer migration plan
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
### Example: start a contextless tangent
|
|
140
140
|
|
|
141
141
|
```text
|
|
142
|
-
/btw
|
|
142
|
+
/unipi:btw-tangent think through this from first principles without using the current chat context
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
### Example: send the result back
|
|
146
146
|
|
|
147
147
|
```text
|
|
148
|
-
/btw
|
|
148
|
+
/unipi:btw-summarize implement the recommended migration plan
|
|
149
149
|
```
|
|
@@ -64,6 +64,7 @@ export interface CocoindexDeps {
|
|
|
64
64
|
const COCOINDEX_STATE_DIR = ".cocoindex";
|
|
65
65
|
const DEFAULT_PIPELINE_DIR = ".unipi/cocoindex";
|
|
66
66
|
const DEFAULT_LANCEDB_PATH = ".unipi/cocoindex/.lancedb";
|
|
67
|
+
const DEFAULT_UPDATE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
67
68
|
const DEFAULT_LEXICAL_SCAN_LIMIT = 50_000;
|
|
68
69
|
|
|
69
70
|
// ─────────────────────────────────────────────────────────
|
|
@@ -268,11 +269,22 @@ export async function indexProject(projectDir: string): Promise<IndexResult> {
|
|
|
268
269
|
const proc = spawn(cocoindexBin, ["update", "main.py"], {
|
|
269
270
|
cwd: pipelineDir,
|
|
270
271
|
stdio: ["pipe", "pipe", "pipe"],
|
|
271
|
-
timeout: 300000, // 5 min timeout
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
let stdout = "";
|
|
275
275
|
let stderr = "";
|
|
276
|
+
let timedOut = false;
|
|
277
|
+
let settled = false;
|
|
278
|
+
const timeoutMs = getUpdateTimeoutMs();
|
|
279
|
+
|
|
280
|
+
const timer = setTimeout(() => {
|
|
281
|
+
timedOut = true;
|
|
282
|
+
proc.kill("SIGTERM");
|
|
283
|
+
setTimeout(() => {
|
|
284
|
+
if (!settled) proc.kill("SIGKILL");
|
|
285
|
+
}, 5000).unref();
|
|
286
|
+
}, timeoutMs);
|
|
287
|
+
timer.unref();
|
|
276
288
|
|
|
277
289
|
proc.stdout.on("data", (data: Buffer) => {
|
|
278
290
|
stdout += data.toString();
|
|
@@ -282,7 +294,9 @@ export async function indexProject(projectDir: string): Promise<IndexResult> {
|
|
|
282
294
|
stderr += data.toString();
|
|
283
295
|
});
|
|
284
296
|
|
|
285
|
-
proc.on("close", (code: number | null) => {
|
|
297
|
+
proc.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
|
298
|
+
settled = true;
|
|
299
|
+
clearTimeout(timer);
|
|
286
300
|
const durationMs = Date.now() - start;
|
|
287
301
|
const chunksProcessed = parseChunksProcessed(stdout);
|
|
288
302
|
|
|
@@ -293,12 +307,14 @@ export async function indexProject(projectDir: string): Promise<IndexResult> {
|
|
|
293
307
|
success: false,
|
|
294
308
|
chunksProcessed,
|
|
295
309
|
durationMs,
|
|
296
|
-
error:
|
|
310
|
+
error: formatIndexFailure({ code, signal, timedOut, timeoutMs, stdout, stderr }),
|
|
297
311
|
});
|
|
298
312
|
}
|
|
299
313
|
});
|
|
300
314
|
|
|
301
315
|
proc.on("error", (err: Error) => {
|
|
316
|
+
settled = true;
|
|
317
|
+
clearTimeout(timer);
|
|
302
318
|
resolve({
|
|
303
319
|
success: false,
|
|
304
320
|
chunksProcessed: 0,
|
|
@@ -310,6 +326,46 @@ export async function indexProject(projectDir: string): Promise<IndexResult> {
|
|
|
310
326
|
}
|
|
311
327
|
|
|
312
328
|
/** Parse the number of files processed from cocoindex v1.0+ output. */
|
|
329
|
+
function getUpdateTimeoutMs(): number {
|
|
330
|
+
const raw = process.env.COCOINDEX_UPDATE_TIMEOUT_MS;
|
|
331
|
+
if (!raw) return DEFAULT_UPDATE_TIMEOUT_MS;
|
|
332
|
+
|
|
333
|
+
const parsed = Number(raw);
|
|
334
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_UPDATE_TIMEOUT_MS;
|
|
335
|
+
return parsed;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function formatIndexFailure(args: {
|
|
339
|
+
code: number | null;
|
|
340
|
+
signal: NodeJS.Signals | null;
|
|
341
|
+
timedOut: boolean;
|
|
342
|
+
timeoutMs: number;
|
|
343
|
+
stdout: string;
|
|
344
|
+
stderr: string;
|
|
345
|
+
}): string {
|
|
346
|
+
const parts: string[] = [];
|
|
347
|
+
|
|
348
|
+
if (args.timedOut) {
|
|
349
|
+
parts.push(`Timed out after ${(args.timeoutMs / 1000).toFixed(0)}s`);
|
|
350
|
+
} else if (args.signal) {
|
|
351
|
+
parts.push(`Process terminated by ${args.signal}`);
|
|
352
|
+
} else {
|
|
353
|
+
parts.push(`Process exited with code ${args.code ?? "unknown"}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const stderr = tailText(args.stderr.trim(), 4000);
|
|
357
|
+
const stdout = tailText(args.stdout.trim(), 2000);
|
|
358
|
+
if (stderr) parts.push(`stderr:\n${stderr}`);
|
|
359
|
+
if (stdout) parts.push(`stdout:\n${stdout}`);
|
|
360
|
+
|
|
361
|
+
return parts.join("\n\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function tailText(text: string, maxChars: number): string {
|
|
365
|
+
if (text.length <= maxChars) return text;
|
|
366
|
+
return `…${text.slice(-maxChars)}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
313
369
|
function parseChunksProcessed(output: string): number {
|
|
314
370
|
// v1.0+ format: "✅ process_file: 604 total | 604 added"
|
|
315
371
|
// Capture the last "added" or "reprocessed" count for process_file
|
|
@@ -321,9 +377,14 @@ function parseChunksProcessed(output: string): number {
|
|
|
321
377
|
}
|
|
322
378
|
}
|
|
323
379
|
if (lastProcessLine) {
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
380
|
+
// Prefer completed work counts. Lines can contain multiple counters, e.g.
|
|
381
|
+
// "process_file: 615 total | 8 added, 606 reprocessed".
|
|
382
|
+
const matches = [...lastProcessLine.matchAll(/(\d+)\s+(?:added|reprocessed|skipped|deleted)/g)];
|
|
383
|
+
const completed = matches.reduce((sum, match) => sum + parseInt(match[1], 10), 0);
|
|
384
|
+
if (completed > 0) return completed;
|
|
385
|
+
|
|
386
|
+
const total = lastProcessLine.match(/process_file:\s*(\d+)\s+total/);
|
|
387
|
+
if (total) return parseInt(total[1], 10);
|
|
327
388
|
}
|
|
328
389
|
|
|
329
390
|
// Fallback: old format "Processed 42 chunks"
|
|
@@ -634,6 +695,8 @@ import os
|
|
|
634
695
|
|
|
635
696
|
# ── Configuration ────────────────────────────────────
|
|
636
697
|
PROJECT_ROOT = os.environ.get("PROJECT_ROOT", "${projectDir}")
|
|
698
|
+
# Safety limit for huge generated/lock files. Set COCO_MAX_FILE_CHARS=0 to disable.
|
|
699
|
+
MAX_FILE_CHARS = int(os.environ.get("COCO_MAX_FILE_CHARS", "200000"))
|
|
637
700
|
|
|
638
701
|
# ── LanceDB context key ──────────────────────────────
|
|
639
702
|
db_key = coco.ContextKey("lancedb/${projectBasename}")
|
|
@@ -703,6 +766,8 @@ async def process_file(
|
|
|
703
766
|
|
|
704
767
|
if not content.strip():
|
|
705
768
|
return
|
|
769
|
+
if MAX_FILE_CHARS > 0 and len(content) > MAX_FILE_CHARS:
|
|
770
|
+
return
|
|
706
771
|
|
|
707
772
|
relative = file.file_path.path.as_posix()
|
|
708
773
|
chunks = await chunk_text(content)
|
|
@@ -750,7 +815,10 @@ async def app_main() -> None:
|
|
|
750
815
|
excluded_patterns=[
|
|
751
816
|
"**/node_modules/**", "**/.git/**", "**/dist/**",
|
|
752
817
|
"**/build/**", "**/.next/**", "**/__pycache__/**",
|
|
753
|
-
"**/.
|
|
818
|
+
"**/coverage/**", "**/.turbo/**", "**/.cache/**",
|
|
819
|
+
"**/.unipi/**",
|
|
820
|
+
"**/*.min.js", "**/bundled.js", "**/bundle.js", "**/*bundle*.js",
|
|
821
|
+
"**/package-lock.json", "**/pnpm-lock.yaml", "**/yarn.lock",
|
|
754
822
|
],
|
|
755
823
|
),
|
|
756
824
|
)
|
|
@@ -13,6 +13,8 @@ import { getLastCompactionStats } from "../compaction/hooks.js";
|
|
|
13
13
|
import { vccRecall } from "../tools/vcc-recall.js";
|
|
14
14
|
import { ctxStats } from "../tools/ctx-stats.js";
|
|
15
15
|
import { ctxDoctor } from "../tools/ctx-doctor.js";
|
|
16
|
+
import { recallBlocksFromContext } from "../session/recall-blocks.js";
|
|
17
|
+
import { filterNoise } from "../compaction/filter-noise.js";
|
|
16
18
|
import type { SessionDB } from "../session/db.js";
|
|
17
19
|
import type { NormalizedBlock, RuntimeCounters } from "../types.js";
|
|
18
20
|
|
|
@@ -90,13 +92,19 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
// ── /unipi:session-recall (new) ─────────────────────
|
|
93
|
-
const sessionRecallHandler = async (args: string, ctx: ExtensionCommandContext) => {
|
|
95
|
+
const sessionRecallHandler = async (args: string, ctx: ExtensionCommandContext, commandName = "/unipi:session-recall") => {
|
|
94
96
|
const query = args.trim();
|
|
95
97
|
if (!query) {
|
|
96
|
-
|
|
98
|
+
const suffix = commandName === "/unipi:compact-recall" ? " (deprecated; use /unipi:session-recall <query>)" : "";
|
|
99
|
+
ctx.ui.notify(`Usage: ${commandName} <query>${suffix}`, "warning");
|
|
97
100
|
return;
|
|
98
101
|
}
|
|
99
|
-
|
|
102
|
+
|
|
103
|
+
// Prefer the live session branch over cached blocks. The branch includes raw
|
|
104
|
+
// pre-compaction messages that are omitted from the compacted LLM context.
|
|
105
|
+
const config = loadConfig((ctx as any).cwd ?? process.cwd());
|
|
106
|
+
const liveBlocks = filterNoise(recallBlocksFromContext(ctx), config.pipeline?.customNoisePatterns);
|
|
107
|
+
const blocks = liveBlocks.length > 0 ? liveBlocks : (deps?.getBlocks() ?? []);
|
|
100
108
|
if (blocks.length === 0) {
|
|
101
109
|
ctx.ui.notify("No session history available for search.", "warning");
|
|
102
110
|
return;
|
|
@@ -120,7 +128,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
120
128
|
description: "(DEPRECATED) Search session history — use /unipi:session-recall instead",
|
|
121
129
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
122
130
|
deprecationLog("/unipi:compact-recall", "/unipi:session-recall");
|
|
123
|
-
return sessionRecallHandler(args, ctx);
|
|
131
|
+
return sessionRecallHandler(args, ctx, "/unipi:compact-recall");
|
|
124
132
|
},
|
|
125
133
|
});
|
|
126
134
|
|
|
@@ -14,6 +14,7 @@ import { registerCommands } from "./commands/index.js";
|
|
|
14
14
|
import { registerCompactorTools } from "./tools/register.js";
|
|
15
15
|
import { normalizeMessages } from "./compaction/normalize.js";
|
|
16
16
|
import { filterNoise } from "./compaction/filter-noise.js";
|
|
17
|
+
import { recallBlocksFromContext } from "./session/recall-blocks.js";
|
|
17
18
|
import type { NormalizedBlock, CompactorStrategyConfig, RuntimeCounters } from "./types.js";
|
|
18
19
|
import type { RuntimeStats } from "./session/analytics.js";
|
|
19
20
|
|
|
@@ -242,12 +243,20 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
242
243
|
// Non-fatal
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
// Re-cache normalized blocks for vcc_recall
|
|
246
|
+
// Re-cache normalized blocks for session_recall/vcc_recall.
|
|
247
|
+
// Command/event contexts do not expose ctx.messages; use the append-only
|
|
248
|
+
// session branch so recall can find raw messages hidden by compaction.
|
|
246
249
|
try {
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
const sessionBlocks = recallBlocksFromContext(ctx);
|
|
251
|
+
if (sessionBlocks.length > 0) {
|
|
252
|
+
cachedBlocks = filterNoise(sessionBlocks, config.pipeline?.customNoisePatterns);
|
|
253
|
+
} else {
|
|
254
|
+
// Defensive fallback for older Pi contexts that happened to expose messages.
|
|
255
|
+
const messages = (ctx as any).messages ?? [];
|
|
256
|
+
if (messages.length > 0) {
|
|
257
|
+
const normalized = normalizeMessages(messages);
|
|
258
|
+
cachedBlocks = filterNoise(normalized, config.pipeline?.customNoisePatterns);
|
|
259
|
+
}
|
|
251
260
|
}
|
|
252
261
|
} catch {
|
|
253
262
|
// Non-fatal: recall will work on empty blocks
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build searchable recall blocks from Pi session entries.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.messages` is not available to command handlers and compacted session
|
|
5
|
+
* context only contains summaries/kept messages. The append-only session branch
|
|
6
|
+
* still contains the raw pre-compaction messages, so recall should index that
|
|
7
|
+
* branch directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
12
|
+
import { normalizeMessages } from "../compaction/normalize.js";
|
|
13
|
+
import { sanitize } from "../compaction/sanitize.js";
|
|
14
|
+
import { textOf } from "../compaction/content.js";
|
|
15
|
+
import type { NormalizedBlock } from "../types.js";
|
|
16
|
+
|
|
17
|
+
function block(kind: "user" | "assistant", text: string, sourceIndex?: number): NormalizedBlock[] {
|
|
18
|
+
const clean = sanitize(text);
|
|
19
|
+
return clean ? [{ kind, text: clean, sourceIndex }] : [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeAgentMessage(message: AgentMessage, sourceIndex: number): NormalizedBlock[] {
|
|
23
|
+
const role = (message as { role?: string }).role;
|
|
24
|
+
|
|
25
|
+
// Standard LLM message roles are handled by the existing normalizer.
|
|
26
|
+
if (role === "user" || role === "assistant" || role === "toolResult") {
|
|
27
|
+
return normalizeMessages([message as Parameters<typeof normalizeMessages>[0][number]]).map((b) => ({
|
|
28
|
+
...b,
|
|
29
|
+
sourceIndex,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pi-specific session message roles that do not participate in pi-ai Message.
|
|
34
|
+
if (role === "bashExecution") {
|
|
35
|
+
const msg = message as AgentMessage & {
|
|
36
|
+
command?: string;
|
|
37
|
+
output?: string;
|
|
38
|
+
exitCode?: number;
|
|
39
|
+
cancelled?: boolean;
|
|
40
|
+
};
|
|
41
|
+
const text = [`$ ${msg.command ?? ""}`, msg.output ?? ""].filter(Boolean).join("\n");
|
|
42
|
+
return text
|
|
43
|
+
? [{ kind: "tool_result", name: "bash", text: sanitize(text), isError: Boolean(msg.cancelled || (msg.exitCode ?? 0) !== 0), sourceIndex }]
|
|
44
|
+
: [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (role === "custom") {
|
|
48
|
+
const msg = message as AgentMessage & { customType?: string; content?: unknown };
|
|
49
|
+
return block("user", [msg.customType, textOf(msg.content)].filter(Boolean).join("\n"), sourceIndex);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (role === "branchSummary") {
|
|
53
|
+
const msg = message as AgentMessage & { summary?: string };
|
|
54
|
+
return block("assistant", msg.summary ?? "", sourceIndex);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (role === "compactionSummary") {
|
|
58
|
+
const msg = message as AgentMessage & { summary?: string };
|
|
59
|
+
return block("assistant", msg.summary ?? "", sourceIndex);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Convert the active append-only session branch into searchable blocks. */
|
|
66
|
+
export function recallBlocksFromSessionEntries(entries: SessionEntry[]): NormalizedBlock[] {
|
|
67
|
+
return entries.flatMap((entry, i) => {
|
|
68
|
+
if (entry.type === "message") {
|
|
69
|
+
return normalizeAgentMessage(entry.message, i);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (entry.type === "custom_message") {
|
|
73
|
+
return block("user", [entry.customType, textOf(entry.content)].filter(Boolean).join("\n"), i);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (entry.type === "branch_summary") {
|
|
77
|
+
return block("assistant", entry.summary, i);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (entry.type === "compaction") {
|
|
81
|
+
return block("assistant", entry.summary, i);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Best-effort read of searchable blocks from an extension context. */
|
|
89
|
+
export function recallBlocksFromContext(ctx: unknown): NormalizedBlock[] {
|
|
90
|
+
const sessionManager = (ctx as { sessionManager?: { getBranch?: () => SessionEntry[]; buildSessionContext?: () => { messages?: AgentMessage[] } } })?.sessionManager;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const entries = sessionManager?.getBranch?.();
|
|
94
|
+
if (Array.isArray(entries) && entries.length > 0) {
|
|
95
|
+
const blocks = recallBlocksFromSessionEntries(entries);
|
|
96
|
+
if (blocks.length > 0) return blocks;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Fall through to compacted context fallback.
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const messages = sessionManager?.buildSessionContext?.().messages ?? [];
|
|
104
|
+
if (messages.length > 0) {
|
|
105
|
+
return messages.flatMap((message, i) => normalizeAgentMessage(message, i));
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// No session context available.
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
18
18
|
import { compactTool } from "./compact.js";
|
|
19
19
|
import { vccRecall, type RecallInput } from "./vcc-recall.js";
|
|
20
20
|
import { ctxExecute, type CtxExecuteInput } from "./ctx-execute.js";
|
|
@@ -23,6 +23,9 @@ import { ctxBatchExecute, type BatchItem } from "./ctx-batch-execute.js";
|
|
|
23
23
|
import { ctxStats, type CtxStatsResult } from "./ctx-stats.js";
|
|
24
24
|
import { ctxDoctor, type DoctorResult } from "./ctx-doctor.js";
|
|
25
25
|
import { contextBudgetTool } from "./context-budget.js";
|
|
26
|
+
import { recallBlocksFromContext } from "../session/recall-blocks.js";
|
|
27
|
+
import { filterNoise } from "../compaction/filter-noise.js";
|
|
28
|
+
import { loadConfig } from "../config/manager.js";
|
|
26
29
|
import type { SessionDB } from "../session/db.js";
|
|
27
30
|
import type { NormalizedBlock, RuntimeCounters } from "../types.js";
|
|
28
31
|
|
|
@@ -157,10 +160,12 @@ export function registerCompactorTools(pi: ExtensionAPI, deps: CompactorToolDeps
|
|
|
157
160
|
} as any));
|
|
158
161
|
|
|
159
162
|
// 2. session_recall (new) / vcc_recall (deprecated) — search session history
|
|
160
|
-
const recallExec = async (_toolCallId: string, params: any): Promise<import("@mariozechner/pi-coding-agent").AgentToolResult<unknown>> => {
|
|
163
|
+
const recallExec = async (_toolCallId: string, params: any, _signal?: AbortSignal, _onUpdate?: unknown, ctx?: ExtensionContext): Promise<import("@mariozechner/pi-coding-agent").AgentToolResult<unknown>> => {
|
|
161
164
|
const c = deps.getCounters?.();
|
|
162
165
|
if (c) { c.recallQueries++; }
|
|
163
|
-
const
|
|
166
|
+
const config = loadConfig(ctx?.cwd ?? process.cwd());
|
|
167
|
+
const liveBlocks = ctx ? filterNoise(recallBlocksFromContext(ctx), config.pipeline?.customNoisePatterns) : [];
|
|
168
|
+
const blocks = liveBlocks.length > 0 ? liveBlocks : deps.getBlocks();
|
|
164
169
|
const input: RecallInput = {
|
|
165
170
|
query: params.query,
|
|
166
171
|
mode: params.mode,
|
|
@@ -236,11 +236,13 @@ export const COMPACTOR_TOOLS = {
|
|
|
236
236
|
export const COMPACTOR_COMMANDS = {
|
|
237
237
|
LOSSLESS_COMPACT: "lossless-compact",
|
|
238
238
|
COMPACT: "compact",
|
|
239
|
+
SESSION_RECALL: "session-recall",
|
|
239
240
|
COMPACT_RECALL: "compact-recall",
|
|
240
241
|
COMPACT_STATS: "compact-stats",
|
|
241
242
|
COMPACT_DOCTOR: "compact-doctor",
|
|
242
243
|
COMPACT_SETTINGS: "compact-settings",
|
|
243
244
|
COMPACT_PRESET: "compact-preset",
|
|
245
|
+
COMPACT_HELP: "compact-help",
|
|
244
246
|
} as const;
|
|
245
247
|
|
|
246
248
|
/** Compactor directory paths */
|
|
@@ -287,6 +289,16 @@ export const NOTIFY_DIRS = {
|
|
|
287
289
|
CONFIG: "~/.unipi/config/notify",
|
|
288
290
|
} as const;
|
|
289
291
|
|
|
292
|
+
/** BTW command names */
|
|
293
|
+
export const BTW_COMMANDS = {
|
|
294
|
+
BTW: "btw",
|
|
295
|
+
TANGENT: "btw-tangent",
|
|
296
|
+
NEW: "btw-new",
|
|
297
|
+
CLEAR: "btw-clear",
|
|
298
|
+
INJECT: "btw-inject",
|
|
299
|
+
SUMMARIZE: "btw-summarize",
|
|
300
|
+
} as const;
|
|
301
|
+
|
|
290
302
|
/** Milestone command names */
|
|
291
303
|
export const MILESTONE_COMMANDS = {
|
|
292
304
|
ONBOARD: "milestone-onboard",
|