@pi-unipi/unipi 2.4.0 → 2.4.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/CHANGELOG.md +43 -0
- package/package.json +26 -22
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +4 -2
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/cocoindex/tools.ts +0 -8
- package/packages/compactor/README.md +18 -22
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/compaction/auto-trigger.ts +0 -0
- package/packages/compactor/src/compaction/build-sections.ts +2 -2
- package/packages/compactor/src/compaction/format.ts +1 -1
- package/packages/compactor/src/config/presets.ts +19 -30
- package/packages/compactor/src/config/schema.ts +0 -0
- package/packages/compactor/src/index.ts +39 -56
- package/packages/compactor/src/session/resume-inject.ts +27 -0
- package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute.ts +4 -2
- package/packages/compactor/src/tools/register.ts +22 -4
- package/packages/compactor/src/tui/settings-overlay.ts +9 -22
- package/packages/compactor/src/types.ts +24 -2
- package/packages/core/events.ts +13 -13
- package/packages/core/package.json +3 -2
- package/packages/footer/README.md +2 -2
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/events.ts +0 -10
- package/packages/footer/src/segments/core.ts +0 -9
- package/packages/image/package.json +2 -2
- package/packages/info-screen/core-groups.ts +0 -14
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/commands.ts +0 -1
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +0 -18
- package/packages/memory/tools.ts +0 -1
- package/packages/milestone/hooks.ts +9 -15
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +2 -0
- package/packages/notify/events.ts +23 -13
- package/packages/notify/package.json +2 -2
- package/packages/notify/tools.ts +17 -8
- package/packages/notify/types.ts +5 -1
- package/packages/ralph/index.ts +10 -5
- package/packages/ralph/package.json +3 -3
- package/packages/ralph/ralph-loop.ts +10 -0
- package/packages/subagents/README.md +3 -1
- package/packages/subagents/package.json +10 -6
- package/packages/subagents/src/agent-manager.ts +46 -11
- package/packages/subagents/src/core-compat.ts +38 -0
- package/packages/subagents/src/global.d.ts +7 -0
- package/packages/subagents/src/index.ts +6 -3
- package/packages/subagents/src/widget.ts +0 -7
- package/packages/unipi/bundled.js +371 -438
- package/packages/updater/package.json +4 -3
- package/packages/utility/README.md +1 -1
- package/packages/utility/package.json +7 -4
- package/packages/utility/src/commands.ts +2 -10
- package/packages/utility/src/diff/theme.ts +0 -3
- package/packages/utility/src/tui/name-badge.ts +0 -9
- package/packages/web-api/package.json +2 -2
- package/packages/web-api/src/engine/extract.ts +0 -26
- package/packages/web-api/src/tools.ts +0 -13
- package/packages/workflow/commands.ts +13 -1
- package/packages/workflow/index.ts +12 -1
- package/packages/workflow/lifecycle.ts +43 -0
- package/packages/workflow/package.json +3 -2
- package/packages/autocomplete/index.ts +0 -7
- package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
- package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
- package/packages/compactor/src/compaction/recall-scope.ts +0 -32
- package/packages/compactor/src/compaction/sections.ts +0 -15
- package/packages/compactor/src/display/bash-display.ts +0 -28
- package/packages/compactor/src/display/diff-presentation.ts +0 -25
- package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
- package/packages/compactor/src/display/user-message-box.ts +0 -16
- package/packages/compactor/src/session/analytics.ts +0 -205
- package/packages/image/index.ts +0 -1
- package/packages/input-shortcuts/index.ts +0 -5
- package/packages/input-shortcuts/src/status.ts +0 -35
- package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
- package/packages/mcp/index.ts +0 -1
- package/packages/memory/search.ts +0 -134
- package/packages/milestone/coexist.ts +0 -114
- package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
- package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
- package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
- package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
- package/packages/subagents/src/__tests__/config.test.ts +0 -240
- package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
- package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
- package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
- package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
- package/packages/subagents/src/prompts.ts +0 -39
- package/packages/utility/index.ts +0 -1
- package/packages/web-api/src/tui/progress.ts +0 -168
- package/packages/web-api/src/tui/result.ts +0 -173
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the 4-tier autocomplete command sorting model.
|
|
3
|
-
*
|
|
4
|
-
* Exercises crossItemPriority() and sortTaggedItems() from sorting.ts.
|
|
5
|
-
* These are pure functions extracted from the provider closure for testability.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from "vitest";
|
|
9
|
-
import {
|
|
10
|
-
crossItemPriority,
|
|
11
|
-
sortTaggedItems,
|
|
12
|
-
type TaggedItem,
|
|
13
|
-
} from "../sorting.js";
|
|
14
|
-
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
15
|
-
|
|
16
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
/** Shorthand to create an AutocompleteItem. */
|
|
19
|
-
function item(value: string, label?: string): AutocompleteItem {
|
|
20
|
-
return { value, label: label ?? value, description: "" };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Shorthand to create a TaggedItem. */
|
|
24
|
-
function tagged(value: string, isUnipi: boolean, label?: string): TaggedItem {
|
|
25
|
-
return { item: item(value, label), isUnipi };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Extract just the values from a sorted TaggedItem[] for easy assertions. */
|
|
29
|
-
function values(sorted: TaggedItem[]): string[] {
|
|
30
|
-
return sorted.map((t) => t.item.value);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
34
|
-
// crossItemPriority — tier assignment
|
|
35
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
36
|
-
|
|
37
|
-
describe("crossItemPriority", () => {
|
|
38
|
-
// ── Tier 0: exact full-value match ──────────────────────────────────
|
|
39
|
-
|
|
40
|
-
it("assigns tier 0 for exact base command match", () => {
|
|
41
|
-
// query "new" matches item value "new" exactly
|
|
42
|
-
expect(crossItemPriority(item("new"), false, "new")).toBe(0);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("assigns tier 0 for exact unipi full-value match", () => {
|
|
46
|
-
// query "unipi:brainstorm" matches item value "unipi:brainstorm"
|
|
47
|
-
expect(crossItemPriority(item("unipi:brainstorm"), true, "unipi:brainstorm")).toBe(0);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("does NOT assign tier 0 for short-name-only match on unipi item", () => {
|
|
51
|
-
// query "brainstorm" is not the full value "unipi:brainstorm"
|
|
52
|
-
expect(crossItemPriority(item("unipi:brainstorm"), true, "brainstorm")).not.toBe(0);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ── Tier 1: unipi short-name exact match ────────────────────────────
|
|
56
|
-
|
|
57
|
-
it("assigns tier 1 when unipi short name matches query exactly", () => {
|
|
58
|
-
expect(crossItemPriority(item("unipi:brainstorm"), true, "brainstorm")).toBe(1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("does NOT assign tier 1 for non-unipi items", () => {
|
|
62
|
-
// "new" is not a unipi item, so short === full === "new", which is tier 0 (exact)
|
|
63
|
-
expect(crossItemPriority(item("new"), false, "new")).toBe(0);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("does NOT assign tier 1 when unipi short name doesn't match", () => {
|
|
67
|
-
expect(crossItemPriority(item("unipi:brainstorm"), true, "brain")).not.toBeLessThanOrEqual(1);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// ── Tier 2: prefix match ────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
it("assigns tier 2 for base prefix match", () => {
|
|
73
|
-
expect(crossItemPriority(item("work"), false, "wor")).toBe(2);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("assigns tier 2 for unipi short-name prefix match", () => {
|
|
77
|
-
// short = "work", query = "wor" → startsWith → tier 2
|
|
78
|
-
expect(crossItemPriority(item("unipi:work"), true, "wor")).toBe(2);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("assigns tier 2 when full value has prefix (unipi: prefix)", () => {
|
|
82
|
-
// full = "unipi:compact", query = "unipi:co" → startsWith → tier 2
|
|
83
|
-
expect(crossItemPriority(item("unipi:compact"), true, "unipi:co")).toBe(2);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// ── Tier 3: fuzzy fallback ──────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
it("assigns tier 3 for fuzzy-only match", () => {
|
|
89
|
-
// "review-work" doesn't start with "wor" but contains characters
|
|
90
|
-
expect(crossItemPriority(item("unipi:review-work"), true, "wor")).toBe(3);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("assigns tier 3 for character subsequence match", () => {
|
|
94
|
-
// "fix" is a subsequence of... no wait, let's use "fx" vs "fix"
|
|
95
|
-
// "fix" contains f, i, x. query "fx" → f...x → subsequence → tier 3
|
|
96
|
-
expect(crossItemPriority(item("fix"), false, "fx")).toBe(3);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("assigns tier 3 for non-matching query (all items get tier 3)", () => {
|
|
100
|
-
expect(crossItemPriority(item("brainstorm"), false, "xyz")).toBe(3);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// ── Case insensitivity ──────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
it("matches case-insensitively (tier 0)", () => {
|
|
106
|
-
expect(crossItemPriority(item("new"), false, "New")).toBe(0);
|
|
107
|
-
expect(crossItemPriority(item("New"), false, "new")).toBe(0);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("matches case-insensitively (tier 2)", () => {
|
|
111
|
-
expect(crossItemPriority(item("Compact"), false, "co")).toBe(2);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("matches case-insensitively (tier 1)", () => {
|
|
115
|
-
expect(crossItemPriority(item("unipi:Brainstorm"), true, "brainstorm")).toBe(1);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
120
|
-
// sortTaggedItems — cross-group merge sort
|
|
121
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
122
|
-
|
|
123
|
-
describe("sortTaggedItems", () => {
|
|
124
|
-
// ── Test 1: Exact base command before fuzzy unipi ───────────────────
|
|
125
|
-
|
|
126
|
-
it("ranks exact base command (tier 0) before fuzzy unipi (tier 3)", () => {
|
|
127
|
-
const items = [
|
|
128
|
-
tagged("unipi:review-work", true), // tier 3 (r→e→w fuzzy)
|
|
129
|
-
tagged("new", false), // tier 0 (exact)
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
const sorted = sortTaggedItems(items, "new");
|
|
133
|
-
expect(values(sorted)).toEqual(["new", "unipi:review-work"]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ── Test 2: Unipi short-name exact (tier 1) ────────────────────────
|
|
137
|
-
|
|
138
|
-
it("ranks unipi short-name exact (tier 1) above prefix and fuzzy", () => {
|
|
139
|
-
const items = [
|
|
140
|
-
tagged("unipi:brainstorm", true), // tier 1
|
|
141
|
-
tagged("unipi:plan", true), // tier 3 (no b-r-a-i-n)
|
|
142
|
-
];
|
|
143
|
-
|
|
144
|
-
const sorted = sortTaggedItems(items, "brainstorm");
|
|
145
|
-
expect(sorted[0].item.value).toBe("unipi:brainstorm");
|
|
146
|
-
expect(crossItemPriority(items[0].item, true, "brainstorm")).toBe(1);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ── Test 3: Full unipi exact (tier 0) ──────────────────────────────
|
|
150
|
-
|
|
151
|
-
it("ranks full unipi: value exact match as tier 0", () => {
|
|
152
|
-
const items = [
|
|
153
|
-
tagged("unipi:brainstorm", true),
|
|
154
|
-
tagged("brainstorm", false), // also tier 0 exact on value
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
const sorted = sortTaggedItems(items, "unipi:brainstorm");
|
|
158
|
-
// Both could be tier 0 or fuzzy. unipi:brainstorm is tier 0 (exact full).
|
|
159
|
-
// "brainstorm" as base: full "brainstorm" !== "unipi:brainstorm" → not tier 0
|
|
160
|
-
// full.startsWith query? "brainstorm".startsWith("unipi:brainstorm") → no
|
|
161
|
-
// So "brainstorm" gets tier 3
|
|
162
|
-
expect(sorted[0].item.value).toBe("unipi:brainstorm");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ── Test 4: Prefix match ordering (non-unipi before unipi) ─────────
|
|
166
|
-
|
|
167
|
-
it("within same tier, sorts non-unipi before unipi", () => {
|
|
168
|
-
const items = [
|
|
169
|
-
tagged("unipi:work", true), // tier 2 (prefix "wor" → "work")
|
|
170
|
-
tagged("work", false), // tier 2 (prefix "wor" → "work")
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
const sorted = sortTaggedItems(items, "wor");
|
|
174
|
-
expect(values(sorted)).toEqual(["work", "unipi:work"]);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ── Test 5: Fuzzy sorted by similarity (shorter name first) ────────
|
|
178
|
-
|
|
179
|
-
it("within tier 3 fuzzy, shorter names sort first", () => {
|
|
180
|
-
const items = [
|
|
181
|
-
tagged("unipi:review-work", true), // longer
|
|
182
|
-
tagged("fix", false), // shorter
|
|
183
|
-
tagged("unipi:fix", true), // medium
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const sorted = sortTaggedItems(items, "fx");
|
|
187
|
-
// All tier 3 fuzzy. Sorted by: non-unipi first, then by length.
|
|
188
|
-
// "fix" (len 3, non-unipi) → "unipi:fix" (len 9, unipi) → "unipi:review-work" (len 17, unipi)
|
|
189
|
-
expect(values(sorted)).toEqual(["fix", "unipi:fix", "unipi:review-work"]);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// ── Test 6: Mixed tiers ────────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
it("sorts across mixed tiers: exact → prefix → fuzzy", () => {
|
|
195
|
-
const items = [
|
|
196
|
-
tagged("unipi:compact", true), // tier 2 (prefix "co")
|
|
197
|
-
tagged("compact-xyz", false), // tier 2 (prefix "co")
|
|
198
|
-
tagged("co", false), // tier 0 (exact "co")
|
|
199
|
-
tagged("unipi:consultant", true), // tier 2 (prefix "co")
|
|
200
|
-
tagged("config", false), // tier 2 (prefix "co")
|
|
201
|
-
tagged("unipi:chore-create", true), // tier 3 (fuzzy c→o)
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
const sorted = sortTaggedItems(items, "co");
|
|
205
|
-
// Tier 0: co
|
|
206
|
-
// Tier 2: compact-xyz, config (non-unipi) then unipi:compact, unipi:consultant (unipi)
|
|
207
|
-
// Tier 3: unipi:chore-create
|
|
208
|
-
const result = values(sorted);
|
|
209
|
-
expect(result[0]).toBe("co");
|
|
210
|
-
// Tier 2 non-unipi
|
|
211
|
-
expect(result.slice(1, 3)).toEqual(expect.arrayContaining(["compact-xyz", "config"]));
|
|
212
|
-
// Tier 2 unipi
|
|
213
|
-
expect(result.slice(3, 5)).toEqual(expect.arrayContaining(["unipi:compact", "unipi:consultant"]));
|
|
214
|
-
// Tier 3
|
|
215
|
-
expect(result[5]).toBe("unipi:chore-create");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// ── Test 7: No base command match, only unipi ──────────────────────
|
|
219
|
-
|
|
220
|
-
it("handles unipi-only matches (tier 1 short-name exact)", () => {
|
|
221
|
-
const items = [
|
|
222
|
-
tagged("unipi:ralph", true), // tier 1 (short "ralph" === "ralph")
|
|
223
|
-
tagged("unipi:ralph-stop", true), // tier 2 (prefix "ralph")
|
|
224
|
-
];
|
|
225
|
-
|
|
226
|
-
const sorted = sortTaggedItems(items, "ralph");
|
|
227
|
-
expect(values(sorted)).toEqual(["unipi:ralph", "unipi:ralph-stop"]);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// ── Test 8: Skill commands filtered (simulated: skill: items present) ─
|
|
231
|
-
|
|
232
|
-
it("sortItems does not filter skill commands — that's the provider's job", () => {
|
|
233
|
-
// The sorting function itself is pure — it just sorts whatever it receives.
|
|
234
|
-
// Skill filtering happens at the provider level. Verify sorting handles them.
|
|
235
|
-
// Note: "skill:animate" does NOT start with "a", so it gets tier 3 (fuzzy).
|
|
236
|
-
// "unipi:auto" short name "auto" starts with "a" → tier 2 (prefix).
|
|
237
|
-
const items = [
|
|
238
|
-
tagged("skill:animate", false),
|
|
239
|
-
tagged("skill:audit", false),
|
|
240
|
-
tagged("unipi:auto", true),
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
const sorted = sortTaggedItems(items, "a");
|
|
244
|
-
// unipi:auto (tier 2) before skill: items (tier 3)
|
|
245
|
-
// Within tier 3: non-unipi, sorted by length: audit(11) < animate(13)
|
|
246
|
-
expect(values(sorted)).toEqual(["unipi:auto", "skill:audit", "skill:animate"]);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// ── Test 9: Skill query scenario ───────────────────────────────────
|
|
250
|
-
|
|
251
|
-
it("when skill: prefix items are present, they sort by tier like any other", () => {
|
|
252
|
-
// "skill:work" does NOT start with "work" → tier 3 (fuzzy)
|
|
253
|
-
// "unipi:work" short name "work" starts with "work" → tier 2 (prefix)
|
|
254
|
-
const items = [
|
|
255
|
-
tagged("skill:work", false), // tier 3 (fuzzy)
|
|
256
|
-
tagged("unipi:work", true), // tier 2 (prefix)
|
|
257
|
-
tagged("skill:workshop", false), // tier 3 (fuzzy)
|
|
258
|
-
];
|
|
259
|
-
|
|
260
|
-
const sorted = sortTaggedItems(items, "work");
|
|
261
|
-
// unipi:work (tier 2) before skill items (tier 3)
|
|
262
|
-
// Within tier 3: non-unipi, sorted by length: skill:work(11) < skill:workshop(15)
|
|
263
|
-
expect(values(sorted)).toEqual(["unipi:work", "skill:work", "skill:workshop"]);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// ── Test 10: Empty query ───────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
it("empty query gives all items tier 0 (exact on empty string)", () => {
|
|
269
|
-
// When query is "", every value matches exactly: "".startsWith("") → true
|
|
270
|
-
// Actually: full === "" is false for non-empty values. But "".startsWith("") is true → tier 2
|
|
271
|
-
// Wait: full === q where q = "". If value is "new", full="new" !== "" → not tier 0.
|
|
272
|
-
// short.startsWith("") → true → tier 2.
|
|
273
|
-
// So all items get tier 2 when query is "".
|
|
274
|
-
// Within tier 2: non-unipi first, then stable.
|
|
275
|
-
const items = [
|
|
276
|
-
tagged("unipi:brain", true),
|
|
277
|
-
tagged("new", false),
|
|
278
|
-
tagged("unipi:work", true),
|
|
279
|
-
tagged("fix", false),
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
const sorted = sortTaggedItems(items, "");
|
|
283
|
-
// All tier 2. Non-unipi first (preserving original order), then unipi.
|
|
284
|
-
expect(values(sorted)).toEqual(["new", "fix", "unipi:brain", "unipi:work"]);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// ── Test 11: Case insensitive ──────────────────────────────────────
|
|
288
|
-
|
|
289
|
-
it("matches case-insensitively across tiers", () => {
|
|
290
|
-
const items = [
|
|
291
|
-
tagged("unipi:Brainstorm", true),
|
|
292
|
-
tagged("New", false),
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
// query "/New" stripped → "new"
|
|
296
|
-
const sorted = sortTaggedItems(items, "new");
|
|
297
|
-
expect(sorted[0].item.value).toBe("New"); // tier 0 exact (case-insensitive)
|
|
298
|
-
|
|
299
|
-
// query "BRAINSTORM" → unipi short-name "brainstorm" exact (case-insensitive)
|
|
300
|
-
const sorted2 = sortTaggedItems(items, "BRAINSTORM");
|
|
301
|
-
expect(sorted2[0].item.value).toBe("unipi:Brainstorm"); // tier 1
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// ── Test 12: Stable sort ───────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
it("preserves original order for items at same tier and same source", () => {
|
|
307
|
-
const items = [
|
|
308
|
-
tagged("alpha", false), // tier 2 (prefix "a")
|
|
309
|
-
tagged("unipi:a-first", true), // tier 2
|
|
310
|
-
tagged("beta", false), // tier 2
|
|
311
|
-
tagged("unipi:a-second", true), // tier 2
|
|
312
|
-
tagged("gamma", false), // tier 2
|
|
313
|
-
];
|
|
314
|
-
|
|
315
|
-
const sorted = sortTaggedItems(items, "a");
|
|
316
|
-
const result = values(sorted);
|
|
317
|
-
|
|
318
|
-
// Non-unipi items preserve their original relative order: alpha, beta, gamma
|
|
319
|
-
const nonUnipi = result.filter((v) => !v.startsWith("unipi:"));
|
|
320
|
-
expect(nonUnipi).toEqual(["alpha", "beta", "gamma"]);
|
|
321
|
-
|
|
322
|
-
// Unipi items preserve their original relative order: a-first, a-second
|
|
323
|
-
const unipiItems = result.filter((v) => v.startsWith("unipi:"));
|
|
324
|
-
expect(unipiItems).toEqual(["unipi:a-first", "unipi:a-second"]);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// ── Test 13: btw:new scenario ──────────────────────────────────────
|
|
328
|
-
|
|
329
|
-
it("btw:new gets tier 3 fuzzy for query 'new' (not prefix match)", () => {
|
|
330
|
-
// "btw:new" does NOT start with "new" → not tier 2
|
|
331
|
-
// "btw:new" !== "new" → not tier 0
|
|
332
|
-
// fuzzy match: b...t...w...:...n...e...w → "new" is subsequence → tier 3
|
|
333
|
-
const items = [
|
|
334
|
-
tagged("btw:new", false), // tier 3
|
|
335
|
-
tagged("new", false), // tier 0
|
|
336
|
-
tagged("unipi:new-thing", true), // tier 2 (prefix "new")
|
|
337
|
-
];
|
|
338
|
-
|
|
339
|
-
const sorted = sortTaggedItems(items, "new");
|
|
340
|
-
expect(values(sorted)).toEqual(["new", "unipi:new-thing", "btw:new"]);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("verifies btw:new is tier 3 for query 'new'", () => {
|
|
344
|
-
expect(crossItemPriority(item("btw:new"), false, "new")).toBe(3);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("verifies 'new' is tier 0 for query 'new'", () => {
|
|
348
|
-
expect(crossItemPriority(item("new"), false, "new")).toBe(0);
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
353
|
-
// Additional edge cases
|
|
354
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
355
|
-
|
|
356
|
-
describe("edge cases", () => {
|
|
357
|
-
it("handles single item", () => {
|
|
358
|
-
const items = [tagged("new", false)];
|
|
359
|
-
const sorted = sortTaggedItems(items, "new");
|
|
360
|
-
expect(values(sorted)).toEqual(["new"]);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("handles empty input array", () => {
|
|
364
|
-
const sorted = sortTaggedItems([], "new");
|
|
365
|
-
expect(sorted).toEqual([]);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("does not mutate the original array", () => {
|
|
369
|
-
const original = [tagged("b", false), tagged("a", false)];
|
|
370
|
-
const copy = [...original];
|
|
371
|
-
sortTaggedItems(original, "a");
|
|
372
|
-
expect(original).toEqual(copy);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("sorts multiple tier 0 exact matches (non-unipi before unipi)", () => {
|
|
376
|
-
const items = [
|
|
377
|
-
tagged("unipi:abc", true), // tier 0 (full value "unipi:abc" === query)
|
|
378
|
-
tagged("unipi:abc", false), // tier 0 (but isUnipi=false, value doesn't start with "unipi:")
|
|
379
|
-
];
|
|
380
|
-
|
|
381
|
-
// Both have value "unipi:abc", but one is tagged isUnipi=true
|
|
382
|
-
// Wait: the non-unipi one has value "unipi:abc" — that's unusual but possible
|
|
383
|
-
// crossItemPriority: full="unipi:abc" === "unipi:abc" → tier 0 for both
|
|
384
|
-
// Same tier → non-unipi first
|
|
385
|
-
const sorted = sortTaggedItems(items, "unipi:abc");
|
|
386
|
-
expect(sorted[0].isUnipi).toBe(false);
|
|
387
|
-
expect(sorted[1].isUnipi).toBe(true);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it("tier 3 sorts by length, non-unipi first within same length", () => {
|
|
391
|
-
const items = [
|
|
392
|
-
tagged("unipi:ab", true), // len 9, tier 3
|
|
393
|
-
tagged("cd", false), // len 2, tier 3
|
|
394
|
-
tagged("ef", false), // len 2, tier 3
|
|
395
|
-
tagged("unipi:gh", true), // len 9, tier 3
|
|
396
|
-
];
|
|
397
|
-
|
|
398
|
-
const sorted = sortTaggedItems(items, "z");
|
|
399
|
-
// All tier 3. Non-unipi first (by length within non-unipi): cd(2), ef(2)
|
|
400
|
-
// Then unipi (by length): unipi:ab(9), unipi:gh(9)
|
|
401
|
-
const result = values(sorted);
|
|
402
|
-
expect(result.slice(0, 2)).toEqual(["cd", "ef"]);
|
|
403
|
-
expect(result.slice(2, 4)).toEqual(["unipi:ab", "unipi:gh"]);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it("handles query matching full unipi prefix + partial short name", () => {
|
|
407
|
-
// query "unipi:bra" → full "unipi:brainstorm".startsWith("unipi:bra") → tier 2
|
|
408
|
-
expect(crossItemPriority(item("unipi:brainstorm"), true, "unipi:bra")).toBe(2);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it("unipi item with tier 1 beats base item with tier 2", () => {
|
|
412
|
-
const items = [
|
|
413
|
-
tagged("work", false), // tier 2 (prefix "brain"... no)
|
|
414
|
-
tagged("unipi:brainstorm", true), // tier 1 (short exact)
|
|
415
|
-
];
|
|
416
|
-
|
|
417
|
-
// query "brainstorm":
|
|
418
|
-
// "work" → full "work" !== "brainstorm", !startsWith → tier 3
|
|
419
|
-
// "unipi:brainstorm" → short "brainstorm" === "brainstorm" → tier 1
|
|
420
|
-
const sorted = sortTaggedItems(items, "brainstorm");
|
|
421
|
-
expect(values(sorted)).toEqual(["unipi:brainstorm", "work"]);
|
|
422
|
-
});
|
|
423
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recall scope — lineage filtering for vcc_recall
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface LineageRange {
|
|
6
|
-
startIndex: number;
|
|
7
|
-
endIndex: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Determine the valid message index range for recall based on
|
|
12
|
-
* the most recent compaction boundary.
|
|
13
|
-
*/
|
|
14
|
-
export function getRecallScope(
|
|
15
|
-
branchEntries: Array<{ type: string; [key: string]: unknown }>,
|
|
16
|
-
opts?: { expand?: boolean },
|
|
17
|
-
): LineageRange {
|
|
18
|
-
let lastCompactionIdx = -1;
|
|
19
|
-
for (let i = branchEntries.length - 1; i >= 0; i--) {
|
|
20
|
-
if (branchEntries[i].type === "compaction") {
|
|
21
|
-
lastCompactionIdx = i;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// If expand is true, include everything; otherwise only post-compaction
|
|
27
|
-
const startIndex = opts?.expand ? 0 : lastCompactionIdx + 1;
|
|
28
|
-
return {
|
|
29
|
-
startIndex: Math.max(0, startIndex),
|
|
30
|
-
endIndex: branchEntries.length,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Section data type for compaction pipeline
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { TranscriptEntry } from "../types.js";
|
|
6
|
-
|
|
7
|
-
export interface SectionData {
|
|
8
|
-
sessionGoal: string[];
|
|
9
|
-
filesAndChanges: string[];
|
|
10
|
-
commits: string[];
|
|
11
|
-
outstandingContext: string[];
|
|
12
|
-
userPreferences: string[];
|
|
13
|
-
briefTranscript: string;
|
|
14
|
-
transcriptEntries: TranscriptEntry[];
|
|
15
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bash display — spinner + elapsed time
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
6
|
-
|
|
7
|
-
export function getSpinnerFrame(index: number): string {
|
|
8
|
-
return SPINNER_FRAMES[index % SPINNER_FRAMES.length];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function formatElapsedTime(ms: number): string {
|
|
12
|
-
if (ms < 1000) return `${ms}ms`;
|
|
13
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
14
|
-
const mins = Math.floor(ms / 60000);
|
|
15
|
-
const secs = ((ms % 60000) / 1000).toFixed(0);
|
|
16
|
-
return `${mins}m${secs}s`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function renderBashCall(command: string, opts?: { collapsed?: boolean }): string {
|
|
20
|
-
if (opts?.collapsed) {
|
|
21
|
-
const firstLine = command.split("\n")[0]?.trim() ?? command;
|
|
22
|
-
if (firstLine.length > 80) {
|
|
23
|
-
return firstLine.slice(0, 77) + "...";
|
|
24
|
-
}
|
|
25
|
-
return firstLine;
|
|
26
|
-
}
|
|
27
|
-
return command;
|
|
28
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diff presentation — layout selection based on terminal width
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { DiffLayout } from "../types.js";
|
|
6
|
-
import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
7
|
-
|
|
8
|
-
export function selectDiffLayout(
|
|
9
|
-
terminalWidth: number,
|
|
10
|
-
preferred: DiffLayout = "auto",
|
|
11
|
-
): "split" | "unified" {
|
|
12
|
-
if (preferred !== "auto") return preferred;
|
|
13
|
-
return terminalWidth >= 100 ? "split" : "unified";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Clamp text to maxWidth visible columns, ANSI-aware */
|
|
17
|
-
export function clampWidth(text: string, maxWidth: number): string {
|
|
18
|
-
return text
|
|
19
|
-
.split("\n")
|
|
20
|
-
.map((line) => {
|
|
21
|
-
if (visibleWidth(line) <= maxWidth) return line;
|
|
22
|
-
return truncateToWidth(line, maxWidth, "…");
|
|
23
|
-
})
|
|
24
|
-
.join("\n");
|
|
25
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pending diff previews during streaming
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface PendingDiffPreviewData {
|
|
6
|
-
type: "edit" | "write" | "create";
|
|
7
|
-
filePath: string;
|
|
8
|
-
previousContent?: string;
|
|
9
|
-
newContent: string;
|
|
10
|
-
confidence: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function buildPendingEditPreviewData(
|
|
14
|
-
filePath: string,
|
|
15
|
-
oldText: string,
|
|
16
|
-
newText: string,
|
|
17
|
-
): PendingDiffPreviewData {
|
|
18
|
-
return {
|
|
19
|
-
type: "edit",
|
|
20
|
-
filePath,
|
|
21
|
-
previousContent: oldText,
|
|
22
|
-
newContent: newText,
|
|
23
|
-
confidence: 1.0,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function buildPendingWritePreviewData(
|
|
28
|
-
filePath: string,
|
|
29
|
-
content: string,
|
|
30
|
-
fileExisted: boolean,
|
|
31
|
-
): PendingDiffPreviewData {
|
|
32
|
-
return {
|
|
33
|
-
type: fileExisted ? "write" : "create",
|
|
34
|
-
filePath,
|
|
35
|
-
newContent: content,
|
|
36
|
-
confidence: 1.0,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function renderPendingDiffPreview(data: PendingDiffPreviewData, opts?: { maxLines?: number }): string {
|
|
41
|
-
const maxLines = opts?.maxLines ?? 20;
|
|
42
|
-
const lines = data.newContent.split("\n").slice(0, maxLines);
|
|
43
|
-
const header = data.type === "edit" ? `✏️ Edit: ${data.filePath}`
|
|
44
|
-
: data.type === "write" ? `📝 Write: ${data.filePath}`
|
|
45
|
-
: `📄 Create: ${data.filePath}`;
|
|
46
|
-
|
|
47
|
-
const omitted = data.newContent.split("\n").length - lines.length;
|
|
48
|
-
const hint = omitted > 0 ? `\n...(${omitted} more lines)...` : "";
|
|
49
|
-
|
|
50
|
-
return `${header}\n${"─".repeat(header.length)}\n${lines.join("\n")}${hint}`;
|
|
51
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* User message box — bordered display for user messages
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export function renderUserMessageBox(text: string, opts?: { maxWidth?: number }): string {
|
|
6
|
-
const maxWidth = opts?.maxWidth ?? 80;
|
|
7
|
-
const lines = text.split("\n");
|
|
8
|
-
const clamped = lines.map((l) => (l.length > maxWidth - 4 ? l.slice(0, maxWidth - 7) + "..." : l));
|
|
9
|
-
const width = Math.min(maxWidth, Math.max(...clamped.map((l) => l.length), 10) + 4);
|
|
10
|
-
|
|
11
|
-
const top = `╭${"─".repeat(width - 2)}╮`;
|
|
12
|
-
const bottom = `╰${"─".repeat(width - 2)}╯`;
|
|
13
|
-
const middle = clamped.map((l) => `│ ${l.padEnd(width - 4)} │`);
|
|
14
|
-
|
|
15
|
-
return [top, ...middle, bottom].join("\n");
|
|
16
|
-
}
|