gentle-pi 2.7.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CommandPaletteGroup, CommandPaletteItem } from "./command-palette.ts";
|
|
2
|
+
|
|
3
|
+
// The curated command set for the command palette: an OpenCode-style
|
|
4
|
+
// grouped menu, not a raw listing of every registered extension command.
|
|
5
|
+
// Only command + label live here; the live description and any shortcut
|
|
6
|
+
// hint are attached at build time by buildCommandPaletteGroups, from the
|
|
7
|
+
// actual registration and the shell's configured shortcuts.
|
|
8
|
+
|
|
9
|
+
export interface CommandPaletteCatalogItem {
|
|
10
|
+
command: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CommandPaletteCatalogGroup {
|
|
15
|
+
title: string;
|
|
16
|
+
items: readonly CommandPaletteCatalogItem[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const COMMAND_PALETTE_CATALOG: readonly CommandPaletteCatalogGroup[] = [
|
|
20
|
+
{
|
|
21
|
+
title: "Configuration",
|
|
22
|
+
items: [
|
|
23
|
+
{ command: "gentle:models", label: "Assign models and effort" },
|
|
24
|
+
{ command: "gentle:profiles", label: "Agent-model profiles" },
|
|
25
|
+
{ command: "gentle:persona", label: "Switch persona" },
|
|
26
|
+
{ command: "gentle:review-mode", label: "Review mode (receipt-driven development)" },
|
|
27
|
+
{ command: "gentle:background-subagents", label: "Background subagents" },
|
|
28
|
+
{ command: "gentle:telemetry", label: "Telemetry" },
|
|
29
|
+
{ command: "gentle:banner", label: "Startup banner" },
|
|
30
|
+
{ command: "gentle:banner-color", label: "Banner color" },
|
|
31
|
+
{ command: "gentle:toggle-rose", label: "Toggle banner rose" },
|
|
32
|
+
{ command: "gentle:toggle-text-logo", label: "Toggle banner text logo" },
|
|
33
|
+
{ command: "gentle:dev-binary", label: "Gentle AI dev binary" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: "Session",
|
|
38
|
+
items: [
|
|
39
|
+
{ command: "gentle:changes", label: "Browse captured changes" },
|
|
40
|
+
{ command: "gentle:agents", label: "Subagents" },
|
|
41
|
+
{ command: "gentle:usage", label: "Subscription usage" },
|
|
42
|
+
{ command: "gentle:review-session-permission", label: "Review session permission" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: "Diagnostics",
|
|
47
|
+
items: [
|
|
48
|
+
{ command: "gentle:status", label: "Gentle AI status" },
|
|
49
|
+
{ command: "gentle:doctor", label: "Doctor" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
title: "SDD",
|
|
54
|
+
items: [
|
|
55
|
+
{ command: "gentle:sdd-preflight", label: "SDD preflight" },
|
|
56
|
+
{ command: "gentle-sdd-status", label: "SDD status" },
|
|
57
|
+
{ command: "gentle-sdd-continue", label: "SDD continue" },
|
|
58
|
+
{ command: "gentle-sdd-init", label: "SDD init" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Skills",
|
|
63
|
+
items: [{ command: "skill-registry:refresh", label: "Refresh skill registry" }],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the palette's groups for one session: keep only catalog entries
|
|
69
|
+
* whose command is actually registered (so a missing extension never shows
|
|
70
|
+
* a dead row), attach that registration's live description and an optional
|
|
71
|
+
* shortcut hint, and drop any group left with no items. Catalog order is
|
|
72
|
+
* preserved throughout.
|
|
73
|
+
*/
|
|
74
|
+
export function buildCommandPaletteGroups(registered: readonly { name: string; description?: string }[], shortcuts: Readonly<Record<string, string | undefined>>): CommandPaletteGroup[] {
|
|
75
|
+
const byName = new Map(registered.map((command) => [command.name, command]));
|
|
76
|
+
const groups: CommandPaletteGroup[] = [];
|
|
77
|
+
for (const group of COMMAND_PALETTE_CATALOG) {
|
|
78
|
+
const items: CommandPaletteItem[] = [];
|
|
79
|
+
for (const entry of group.items) {
|
|
80
|
+
const found = byName.get(entry.command);
|
|
81
|
+
if (!found) continue;
|
|
82
|
+
items.push({ command: entry.command, label: entry.label, description: found.description, shortcut: shortcuts[entry.command] });
|
|
83
|
+
}
|
|
84
|
+
if (items.length > 0) groups.push({ title: group.title, items });
|
|
85
|
+
}
|
|
86
|
+
return groups;
|
|
87
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { isKeyRelease, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { stripAnsi } from "./terminal-theme.ts";
|
|
3
|
+
|
|
4
|
+
// Command palette: a pure, curated, grouped overlay component for
|
|
5
|
+
// `/gentle:commands` and its `alt+k` shortcut (see
|
|
6
|
+
// extensions/gentle-shell.ts). It only knows about plain groups of items
|
|
7
|
+
// (command + label, plus an optional description used for ranking and an
|
|
8
|
+
// optional shortcut hint), never about the Pi extension API, so it can be
|
|
9
|
+
// fully exercised without a running agent session. The curated catalog
|
|
10
|
+
// itself lives in lib/command-palette-catalog.ts.
|
|
11
|
+
|
|
12
|
+
export interface CommandPaletteItem {
|
|
13
|
+
command: string;
|
|
14
|
+
label: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
shortcut?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CommandPaletteGroup {
|
|
20
|
+
title: string;
|
|
21
|
+
items: CommandPaletteItem[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CommandPaletteResult = { type: "run"; name: string } | { type: "close" };
|
|
25
|
+
|
|
26
|
+
export interface CommandPaletteTheme {
|
|
27
|
+
fg(color: string, text: string): string;
|
|
28
|
+
/** Optional: when present, the selected row is drawn as a full-width
|
|
29
|
+
* highlighted bar via bg("selectedBg", ...) instead of a `▸` marker. */
|
|
30
|
+
bg?(color: string, text: string): string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// stripAnsi comes from ./terminal-theme.ts (identical intent: strip ANSI/OSC
|
|
34
|
+
// escape sequences before measuring or comparing text). sanitizeTerminalText
|
|
35
|
+
// stays a local copy on purpose: the shared lib/terminal-theme.ts version
|
|
36
|
+
// deletes control characters, but this palette escapes them into a visible
|
|
37
|
+
// "\xNN" form instead (same regex as the local copy in
|
|
38
|
+
// extensions/gentle-agents.ts), so a stray control byte in a label,
|
|
39
|
+
// shortcut, or description is never silently dropped.
|
|
40
|
+
function sanitizeTerminalText(value: string): string {
|
|
41
|
+
return value.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, (control) => `\\x${control.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isSubsequence(query: string, text: string): boolean {
|
|
45
|
+
let index = 0;
|
|
46
|
+
for (const char of text) {
|
|
47
|
+
if (index < query.length && char === query[index]) index += 1;
|
|
48
|
+
}
|
|
49
|
+
return index === query.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function rankItem(item: CommandPaletteItem, lowerQuery: string): number | undefined {
|
|
53
|
+
const label = item.label.toLowerCase();
|
|
54
|
+
if (label.startsWith(lowerQuery)) return 0;
|
|
55
|
+
if (label.includes(lowerQuery)) return 1;
|
|
56
|
+
if (isSubsequence(lowerQuery, label)) return 2;
|
|
57
|
+
if (item.command.toLowerCase().includes(lowerQuery)) return 3;
|
|
58
|
+
if (item.description && item.description.toLowerCase().includes(lowerQuery)) return 4;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Rank each group's items against a query: 0 label starts with query, 1
|
|
64
|
+
* label contains, 2 label matches as a subsequence, 3 command name
|
|
65
|
+
* contains, 4 description contains. Matching is case-insensitive and the
|
|
66
|
+
* query is trimmed; ties keep input order within a group (stable sort).
|
|
67
|
+
* Groups are never reordered relative to each other, and a group left with
|
|
68
|
+
* no matching item is dropped entirely. An empty or whitespace-only query
|
|
69
|
+
* returns every group unchanged.
|
|
70
|
+
*/
|
|
71
|
+
export function rankPaletteGroups(groups: readonly CommandPaletteGroup[], query: string): CommandPaletteGroup[] {
|
|
72
|
+
const trimmed = query.trim();
|
|
73
|
+
if (!trimmed) return groups.map((group) => ({ title: group.title, items: [...group.items] }));
|
|
74
|
+
const lower = trimmed.toLowerCase();
|
|
75
|
+
const result: CommandPaletteGroup[] = [];
|
|
76
|
+
for (const group of groups) {
|
|
77
|
+
const ranked: { item: CommandPaletteItem; rank: number }[] = [];
|
|
78
|
+
for (const item of group.items) {
|
|
79
|
+
const rank = rankItem(item, lower);
|
|
80
|
+
if (rank !== undefined) ranked.push({ item, rank });
|
|
81
|
+
}
|
|
82
|
+
if (ranked.length === 0) continue;
|
|
83
|
+
ranked.sort((a, b) => a.rank - b.rank);
|
|
84
|
+
result.push({ title: group.title, items: ranked.map((entry) => entry.item) });
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type Tone = "border" | "muted" | "text" | "title" | "accent";
|
|
90
|
+
|
|
91
|
+
const TONE_COLOR: Record<Tone, string> = {
|
|
92
|
+
border: "border",
|
|
93
|
+
muted: "muted",
|
|
94
|
+
text: "text",
|
|
95
|
+
title: "accent",
|
|
96
|
+
accent: "accent",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Two border glyphs ("│ " and " │") plus the padding space on each side.
|
|
100
|
+
const CARD_PADDING = 4;
|
|
101
|
+
const CARD_BORDER_ROWS = 2;
|
|
102
|
+
// Fixed content rows outside the grouped match list: header, query/search,
|
|
103
|
+
// a blank row, a blank row before the footer, and the footer itself.
|
|
104
|
+
const CHROME_ROWS = 5;
|
|
105
|
+
const SCROLL_INDICATOR_ROWS = 2;
|
|
106
|
+
// The card never shrinks below this many total lines, and otherwise fills
|
|
107
|
+
// up to this fraction of the terminal's rows (see render()). There is no
|
|
108
|
+
// fixed row cap here: that belongs to the models panel only.
|
|
109
|
+
const MIN_TOTAL_ROWS = 12;
|
|
110
|
+
const HEIGHT_RATIO = 0.85;
|
|
111
|
+
const DEFAULT_ROWS = 40;
|
|
112
|
+
|
|
113
|
+
type PaletteRow = { kind: "title"; text: string } | { kind: "blank" } | { kind: "item"; item: CommandPaletteItem; itemIndex: number };
|
|
114
|
+
|
|
115
|
+
function buildRows(groups: readonly CommandPaletteGroup[]): PaletteRow[] {
|
|
116
|
+
const rows: PaletteRow[] = [];
|
|
117
|
+
let itemIndex = 0;
|
|
118
|
+
groups.forEach((group, groupIndex) => {
|
|
119
|
+
if (groupIndex > 0) rows.push({ kind: "blank" });
|
|
120
|
+
rows.push({ kind: "title", text: group.title });
|
|
121
|
+
for (const item of group.items) {
|
|
122
|
+
rows.push({ kind: "item", item, itemIndex });
|
|
123
|
+
itemIndex += 1;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return rows;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function countItems(groups: readonly CommandPaletteGroup[]): number {
|
|
130
|
+
return groups.reduce((sum, group) => sum + group.items.length, 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function itemAt(groups: readonly CommandPaletteGroup[], index: number): CommandPaletteItem | undefined {
|
|
134
|
+
let cursor = 0;
|
|
135
|
+
for (const group of groups) {
|
|
136
|
+
if (index < cursor + group.items.length) return group.items[index - cursor];
|
|
137
|
+
cursor += group.items.length;
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class CommandPalette {
|
|
143
|
+
private query = "";
|
|
144
|
+
private selected = 0;
|
|
145
|
+
private completed = false;
|
|
146
|
+
private readonly groups: readonly CommandPaletteGroup[];
|
|
147
|
+
private readonly done: (result: CommandPaletteResult) => void;
|
|
148
|
+
private readonly theme: CommandPaletteTheme | undefined;
|
|
149
|
+
private readonly rowsFn: () => number;
|
|
150
|
+
|
|
151
|
+
constructor(groups: readonly CommandPaletteGroup[], done: (result: CommandPaletteResult) => void, theme?: CommandPaletteTheme, rows: () => number = () => DEFAULT_ROWS) {
|
|
152
|
+
this.groups = groups;
|
|
153
|
+
this.done = done;
|
|
154
|
+
this.theme = theme;
|
|
155
|
+
this.rowsFn = rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
invalidate(): void {}
|
|
159
|
+
|
|
160
|
+
handleInput(data: string): void {
|
|
161
|
+
if (this.completed || isKeyRelease(data)) return;
|
|
162
|
+
// A bare line feed ("\n") is both ctrl+j and, on non-Kitty terminals,
|
|
163
|
+
// a synonym for Enter: check the scroll/move keys first so ctrl+j
|
|
164
|
+
// always scrolls instead of running the highlighted command (same
|
|
165
|
+
// ordering as lib/shell-changes-view.ts).
|
|
166
|
+
if (matchesKey(data, "down") || matchesKey(data, "ctrl+j")) {
|
|
167
|
+
this.moveSelection(1);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (matchesKey(data, "up") || matchesKey(data, "ctrl+k")) {
|
|
171
|
+
this.moveSelection(-1);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
175
|
+
this.finish({ type: "close" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
179
|
+
const item = itemAt(this.matches(), this.selected);
|
|
180
|
+
if (item) this.finish({ type: "run", name: item.command });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (matchesKey(data, "backspace")) {
|
|
184
|
+
if (this.query.length > 0) this.setQuery(this.query.slice(0, -1));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (matchesKey(data, "ctrl+u")) {
|
|
188
|
+
if (this.query.length > 0) this.setQuery("");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (this.isSingleBmpPrintable(data)) this.setQuery(this.query + data);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
render(width: number): string[] {
|
|
195
|
+
const innerWidth = Math.max(1, width - CARD_PADDING);
|
|
196
|
+
const maxTotalLines = Math.max(MIN_TOTAL_ROWS, Math.floor(this.rowsFn() * HEIGHT_RATIO));
|
|
197
|
+
return this.renderCard(this.renderBody(innerWidth, maxTotalLines), innerWidth);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* True for a single Basic-Multilingual-Plane printable character.
|
|
202
|
+
* Multi-character input (a paste delivered as one `handleInput` call)
|
|
203
|
+
* and astral characters (surrogate pairs, where `data.length === 2`)
|
|
204
|
+
* are ignored on purpose: the palette only appends one keystroke at a
|
|
205
|
+
* time, and a raw terminal paste is not a single keystroke.
|
|
206
|
+
*/
|
|
207
|
+
private isSingleBmpPrintable(data: string): boolean {
|
|
208
|
+
if (data.length !== 1) return false;
|
|
209
|
+
const codePoint = data.codePointAt(0) ?? 0;
|
|
210
|
+
return codePoint >= 0x20 && codePoint !== 0x7f;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private setQuery(query: string): void {
|
|
214
|
+
this.query = query;
|
|
215
|
+
this.selected = 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private moveSelection(delta: number): void {
|
|
219
|
+
const total = countItems(this.matches());
|
|
220
|
+
if (total === 0) return;
|
|
221
|
+
this.selected = Math.max(0, Math.min(total - 1, this.selected + delta));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private matches(): CommandPaletteGroup[] {
|
|
225
|
+
return rankPaletteGroups(this.groups, this.query);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private finish(result: CommandPaletteResult): void {
|
|
229
|
+
if (this.completed) return;
|
|
230
|
+
this.completed = true;
|
|
231
|
+
this.done(result);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private renderBody(width: number, maxTotalLines: number): string[] {
|
|
235
|
+
const lines: string[] = [];
|
|
236
|
+
lines.push(this.renderHeaderRow(width));
|
|
237
|
+
lines.push(this.query.length > 0 ? this.renderLine(`› ${this.query}`, width) : this.renderLine("› Search", width, "muted"));
|
|
238
|
+
lines.push("");
|
|
239
|
+
const groups = this.matches();
|
|
240
|
+
if (groups.length === 0) {
|
|
241
|
+
lines.push(this.renderLine(`No commands match "${this.query}"`, width, "muted"));
|
|
242
|
+
} else {
|
|
243
|
+
const budget = Math.max(1, maxTotalLines - CARD_BORDER_ROWS - CHROME_ROWS);
|
|
244
|
+
lines.push(...this.renderGroups(groups, width, budget));
|
|
245
|
+
}
|
|
246
|
+
lines.push("");
|
|
247
|
+
lines.push(this.renderLine("type to search • ↑/↓ move • enter run • esc close", width, "muted"));
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private renderHeaderRow(width: number): string {
|
|
252
|
+
const left = this.renderText("Commands", "title");
|
|
253
|
+
const right = this.renderText("esc", "muted");
|
|
254
|
+
return this.justifyToWidth(left, right, width);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private renderGroups(groups: CommandPaletteGroup[], width: number, budget: number): string[] {
|
|
258
|
+
const rows = buildRows(groups);
|
|
259
|
+
const selected = Math.max(0, Math.min(countItems(groups) - 1, this.selected));
|
|
260
|
+
if (rows.length <= budget) return rows.map((row) => this.renderRow(row, width, selected));
|
|
261
|
+
const windowSize = Math.max(1, budget - SCROLL_INDICATOR_ROWS);
|
|
262
|
+
const selectedRowIndex = Math.max(0, rows.findIndex((row) => row.kind === "item" && row.itemIndex === selected));
|
|
263
|
+
const start = Math.max(0, Math.min(selectedRowIndex - Math.floor(windowSize / 2), Math.max(0, rows.length - windowSize)));
|
|
264
|
+
const end = Math.min(rows.length, start + windowSize);
|
|
265
|
+
const result: string[] = [];
|
|
266
|
+
if (start > 0) result.push(this.renderLine(` ↑ ${start} more`, width, "muted"));
|
|
267
|
+
for (let i = start; i < end; i++) {
|
|
268
|
+
const row = rows[i];
|
|
269
|
+
if (row) result.push(this.renderRow(row, width, selected));
|
|
270
|
+
}
|
|
271
|
+
if (end < rows.length) result.push(this.renderLine(` ↓ ${rows.length - end} more`, width, "muted"));
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private renderRow(row: PaletteRow, width: number, selected: number): string {
|
|
276
|
+
if (row.kind === "blank") return "";
|
|
277
|
+
if (row.kind === "title") return this.renderLine(row.text, width, "accent");
|
|
278
|
+
return this.renderItemRow(row.item, row.itemIndex === selected, width);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private renderItemRow(item: CommandPaletteItem, focused: boolean, width: number): string {
|
|
282
|
+
const canHighlightBg = focused && this.theme?.bg !== undefined;
|
|
283
|
+
const label = sanitizeTerminalText(item.label);
|
|
284
|
+
const shortcut = item.shortcut ? sanitizeTerminalText(item.shortcut) : "";
|
|
285
|
+
if (canHighlightBg) {
|
|
286
|
+
const plainRow = this.justifyToWidth(` ${label}`, shortcut, width);
|
|
287
|
+
return this.theme?.bg?.("selectedBg", plainRow) ?? plainRow;
|
|
288
|
+
}
|
|
289
|
+
const prefix = focused ? this.renderText("▸ ", "accent") : " ";
|
|
290
|
+
const prefixWidth = visibleWidth(stripAnsi(prefix));
|
|
291
|
+
const availableWidth = Math.max(0, width - prefixWidth);
|
|
292
|
+
const labelStyled = this.renderText(label, "text");
|
|
293
|
+
const shortcutStyled = shortcut ? this.renderText(shortcut, "muted") : "";
|
|
294
|
+
return `${prefix}${this.justifyToWidth(labelStyled, shortcutStyled, availableWidth)}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Left-align `left`, right-align `right`, and pad to exactly `width`
|
|
298
|
+
* (display columns) so a caller can wrap the whole result in a
|
|
299
|
+
* full-width background highlight without a ragged tail. */
|
|
300
|
+
private justifyToWidth(left: string, right: string, width: number): string {
|
|
301
|
+
const leftWidth = visibleWidth(stripAnsi(left));
|
|
302
|
+
const rightWidth = visibleWidth(stripAnsi(right));
|
|
303
|
+
const minGap = right ? 1 : 0;
|
|
304
|
+
const gap = Math.max(minGap, width - leftWidth - rightWidth);
|
|
305
|
+
const used = leftWidth + gap + rightWidth;
|
|
306
|
+
const trailingPad = Math.max(0, width - used);
|
|
307
|
+
return `${left}${" ".repeat(gap)}${right}${" ".repeat(trailingPad)}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private renderCard(lines: string[], innerWidth: number): string[] {
|
|
311
|
+
const horizontal = "─".repeat(innerWidth + 2);
|
|
312
|
+
const border = (text: string) => this.renderText(text, "border");
|
|
313
|
+
return [border(`╭${horizontal}╮`), ...lines.map((content) => `${border("│")} ${this.fitStyledLine(content, innerWidth)} ${border("│")}`), border(`╰${horizontal}╯`)];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private fitStyledLine(content: string, width: number): string {
|
|
317
|
+
const visible = visibleWidth(stripAnsi(content));
|
|
318
|
+
if (visible > width) return truncateToWidth(content, Math.max(1, width), "…", true);
|
|
319
|
+
return `${content}${" ".repeat(Math.max(0, width - visible))}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private renderLine(text = "", width: number, tone?: Tone): string {
|
|
323
|
+
const safe = truncateToWidth(sanitizeTerminalText(text), Math.max(1, width), "…", true);
|
|
324
|
+
return tone ? this.renderText(safe, tone) : safe;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private renderText(text: string, tone: Tone): string {
|
|
328
|
+
const safe = sanitizeTerminalText(text);
|
|
329
|
+
if (!this.theme) return safe;
|
|
330
|
+
return this.theme.fg(TONE_COLOR[tone], safe);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Shortcut for `/gentle:commands`. Reads GENTLE_PI_COMMANDS_KEY: unset
|
|
336
|
+
* defaults to "alt+k" (ctrl+k is reserved by Pi's editor for
|
|
337
|
+
* delete-to-line-end, so an extension shortcut on it is skipped); an empty
|
|
338
|
+
* value or "off" (case-insensitive) disables the shortcut; anything else is
|
|
339
|
+
* used as-is (trimmed). Same shape as changesShortcut in
|
|
340
|
+
* extensions/gentle-shell.ts.
|
|
341
|
+
*/
|
|
342
|
+
export function commandsKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
343
|
+
const value = env.GENTLE_PI_COMMANDS_KEY?.trim();
|
|
344
|
+
if (value === undefined) return "alt+k";
|
|
345
|
+
return value === "" || value.toLowerCase() === "off" ? undefined : value;
|
|
346
|
+
}
|
|
@@ -62,6 +62,11 @@ export class NativeChoiceList<T extends NativeChoiceItem> extends Container {
|
|
|
62
62
|
this.refreshRows();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Refresh rendered rows after callers update the existing item records. */
|
|
66
|
+
refreshItems(): void {
|
|
67
|
+
this.refreshRows();
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
clearHover(): boolean {
|
|
66
71
|
if (!this.hovered) return false;
|
|
67
72
|
this.hovered = undefined;
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -68,7 +68,6 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
68
68
|
CAPTURE_UNACHIEVABLE: "review/capture-unachievable",
|
|
69
69
|
ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
|
|
70
70
|
SDD_STATUS: "sdd-status",
|
|
71
|
-
SDD_ATTEMPT: "sdd-attempt",
|
|
72
71
|
SDD_CONTINUE: "sdd-continue",
|
|
73
72
|
} as const;
|
|
74
73
|
export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
|
|
@@ -127,39 +126,6 @@ export interface NativeReviewCli {
|
|
|
127
126
|
// delivery or review-transaction authority.
|
|
128
127
|
sddStatus?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
|
|
129
128
|
sddContinue?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
|
|
130
|
-
sddAttemptAcquire?(request: NativeSddAcquireRequest): Promise<NativeSddAttemptResult>;
|
|
131
|
-
sddAttemptSettle?(request: NativeSddSettleRequest): Promise<NativeSddAttemptResult>;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export interface NativeSddAttemptRequest extends Pick<NativeUntrackedSelectionRequest, "untrackedScope" | "expectedUntrackedInventory" | "intendedUntracked"> {
|
|
135
|
-
workspaceRoot: string;
|
|
136
|
-
changeName: string;
|
|
137
|
-
requestId: string;
|
|
138
|
-
remediatesEvidenceRevision?: string;
|
|
139
|
-
}
|
|
140
|
-
export interface NativeSddAcquireRequest extends NativeSddAttemptRequest {
|
|
141
|
-
workUnit: string;
|
|
142
|
-
evidenceGoal: string;
|
|
143
|
-
maxAttempts?: number;
|
|
144
|
-
maxChangedLines?: number;
|
|
145
|
-
expectedRevision?: string;
|
|
146
|
-
token?: string;
|
|
147
|
-
}
|
|
148
|
-
export const SDD_ATTEMPT_OUTCOME = { PASSED: "passed", FAILED: "failed", INTERRUPTED: "interrupted" } as const;
|
|
149
|
-
export interface NativeSddSettleRequest extends NativeSddAttemptRequest {
|
|
150
|
-
token: string;
|
|
151
|
-
outcome: (typeof SDD_ATTEMPT_OUTCOME)[keyof typeof SDD_ATTEMPT_OUTCOME];
|
|
152
|
-
evidenceRevision?: string;
|
|
153
|
-
remediationEvidence?: string;
|
|
154
|
-
diagnosis: string;
|
|
155
|
-
harnessDisposition: "reused" | "invalidated";
|
|
156
|
-
cleanupEvidence: string;
|
|
157
|
-
processEvidence: string;
|
|
158
|
-
}
|
|
159
|
-
export interface NativeSddAttemptResult {
|
|
160
|
-
state: "proceed" | "blocked" | "complete";
|
|
161
|
-
token?: string;
|
|
162
|
-
reason?: string;
|
|
163
129
|
}
|
|
164
130
|
|
|
165
131
|
export const NATIVE_REVIEW_MODE_OPERATION = {
|
|
@@ -219,7 +185,7 @@ export interface NativeSddStatusV2 extends Readonly<Record<string, unknown>> {
|
|
|
219
185
|
changeRoot: string | null;
|
|
220
186
|
actionContext: Readonly<Record<string, unknown>> & { mode: "repo-local"; workspaceRoot: string; allowedEditRoots: readonly string[] };
|
|
221
187
|
dependencies: Readonly<Record<(typeof NATIVE_SDD_DEPENDENCIES)[number], NativeSddDependencyState>>;
|
|
222
|
-
phaseInstructions?: Readonly<Record<(typeof NATIVE_SDD_INSTRUCTION_PHASES)[number], readonly string[]
|
|
188
|
+
phaseInstructions?: Readonly<Record<(typeof NATIVE_SDD_INSTRUCTION_PHASES)[number], readonly string[]> & { remediate?: readonly string[] }>;
|
|
223
189
|
blockedReasons: readonly string[];
|
|
224
190
|
nextRecommended: string;
|
|
225
191
|
remediationState?: { required: boolean; complete: boolean; failedEvidenceRevision: string };
|
|
@@ -1033,7 +999,7 @@ export class NativeReviewCliError extends Error {
|
|
|
1033
999
|
this.launchAttempted = launchAttempted;
|
|
1034
1000
|
this.mutating = mutating;
|
|
1035
1001
|
this.mutationOutcome = launchAttempted && mutating ? "unknown" : "none";
|
|
1036
|
-
this.nextAction = this.mutationOutcome === "unknown"
|
|
1002
|
+
this.nextAction = this.mutationOutcome === "unknown" ? "review.status" : undefined;
|
|
1037
1003
|
this.diagnostics = diagnostics ?? { operation, error_code: code, timed_out: false, output_limit_exceeded: false };
|
|
1038
1004
|
this.auditRecord = auditRecord;
|
|
1039
1005
|
}
|
|
@@ -1459,7 +1425,7 @@ interface NativeJsonExecution {
|
|
|
1459
1425
|
}
|
|
1460
1426
|
|
|
1461
1427
|
const NATIVE_SDD_DEPENDENCIES = ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"] as const;
|
|
1462
|
-
const NATIVE_SDD_INSTRUCTION_PHASES = ["apply", "verify", "
|
|
1428
|
+
const NATIVE_SDD_INSTRUCTION_PHASES = ["apply", "verify", "archive"] as const;
|
|
1463
1429
|
const NATIVE_SDD_NEXT_RECOMMENDATIONS = ["apply", "verify", "remediate", "archive", "archived", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] as const;
|
|
1464
1430
|
const NATIVE_SDD_DEPENDENCY_STATES = ["blocked", "ready", "all_done"] as const;
|
|
1465
1431
|
|
|
@@ -1487,7 +1453,13 @@ export function decodeNativeSddStatusV2(value: unknown, request: Pick<NativeSddS
|
|
|
1487
1453
|
if (status.phaseInstructions !== undefined) {
|
|
1488
1454
|
const instructions = object(status.phaseInstructions);
|
|
1489
1455
|
for (const phase of NATIVE_SDD_INSTRUCTION_PHASES) stringArray(instructions[phase]);
|
|
1490
|
-
|
|
1456
|
+
// Classical SDD no longer emits a remediation phase. Keep the published
|
|
1457
|
+
// producer's optional legacy instructions intact without inventing them
|
|
1458
|
+
// for a newer producer or accepting unknown phase keys.
|
|
1459
|
+
const hasRemediation = Object.hasOwn(instructions, "remediate");
|
|
1460
|
+
if (hasRemediation) stringArray(instructions.remediate);
|
|
1461
|
+
if (Object.keys(instructions).length !== NATIVE_SDD_INSTRUCTION_PHASES.length + Number(hasRemediation)) throw new Error("native SDD instructions have an unsupported shape");
|
|
1462
|
+
if (status.nextRecommended === "remediate" && !hasRemediation) throw new Error("native SDD remediation instructions are missing");
|
|
1491
1463
|
}
|
|
1492
1464
|
if (status.nextRecommended === "remediate" || status.remediationState !== undefined) {
|
|
1493
1465
|
const remediation = object(status.remediationState);
|
|
@@ -2136,65 +2108,6 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2136
2108
|
return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
|
|
2137
2109
|
}
|
|
2138
2110
|
|
|
2139
|
-
async sddAttemptAcquire(request: NativeSddAcquireRequest): Promise<NativeSddAttemptResult> {
|
|
2140
|
-
return this.sddAttempt("acquire", request);
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
async sddAttemptSettle(request: NativeSddSettleRequest): Promise<NativeSddAttemptResult> {
|
|
2144
|
-
return this.sddAttempt("settle", request);
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
private async sddAttempt(verb: "acquire" | "settle", request: NativeSddAcquireRequest | NativeSddSettleRequest): Promise<NativeSddAttemptResult> {
|
|
2148
|
-
const args = ["sdd-attempt", verb, "--cwd", request.workspaceRoot, "--change", request.changeName, "--request-id", request.requestId];
|
|
2149
|
-
if (!isAbsolute(request.workspaceRoot) || !isCanonicalProcessString(request.workspaceRoot) || !isCanonicalProcessString(request.changeName) || !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(request.requestId)) throw new TypeError("Invalid SDD attempt identity");
|
|
2150
|
-
const text = (flag: string, value: string, max: number) => {
|
|
2151
|
-
if (!isCanonicalProcessString(value) || /[\r\n]/.test(value) || Buffer.byteLength(value) > max) throw new TypeError(`Invalid ${flag}`);
|
|
2152
|
-
args.push(flag, value);
|
|
2153
|
-
};
|
|
2154
|
-
const revision = (flag: string, value: string | undefined) => {
|
|
2155
|
-
if (value === undefined) return;
|
|
2156
|
-
if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new TypeError(`Invalid ${flag}`);
|
|
2157
|
-
args.push(flag, value);
|
|
2158
|
-
};
|
|
2159
|
-
if (verb === "acquire") {
|
|
2160
|
-
const acquire = request as NativeSddAcquireRequest;
|
|
2161
|
-
text("--work-unit", acquire.workUnit, 160);
|
|
2162
|
-
text("--evidence-goal", acquire.evidenceGoal, 240);
|
|
2163
|
-
for (const [flag, value, max] of [["--max-attempts", acquire.maxAttempts, 100], ["--max-changed-lines", acquire.maxChangedLines, 1_000_000]] as const) {
|
|
2164
|
-
if (value === undefined) continue;
|
|
2165
|
-
if (!Number.isInteger(value) || value < 1 || value > max) throw new TypeError(`Invalid ${flag}`);
|
|
2166
|
-
args.push(flag, String(value));
|
|
2167
|
-
}
|
|
2168
|
-
if (acquire.expectedRevision === "") args.push("--expected-revision", "");
|
|
2169
|
-
else revision("--expected-revision", acquire.expectedRevision);
|
|
2170
|
-
if (acquire.token !== undefined) text("--token", acquire.token, 500);
|
|
2171
|
-
} else {
|
|
2172
|
-
const settle = request as NativeSddSettleRequest;
|
|
2173
|
-
text("--token", settle.token, 500);
|
|
2174
|
-
if (!Object.values(SDD_ATTEMPT_OUTCOME).includes(settle.outcome)) throw new TypeError("Invalid outcome");
|
|
2175
|
-
args.push("--outcome", settle.outcome);
|
|
2176
|
-
if (settle.outcome === "interrupted" ? settle.evidenceRevision !== undefined || settle.remediationEvidence !== undefined : !settle.evidenceRevision && !(settle.outcome === "passed" && settle.remediationEvidence)) throw new TypeError("Invalid terminal evidence");
|
|
2177
|
-
revision("--evidence-revision", settle.evidenceRevision);
|
|
2178
|
-
text("--diagnosis", settle.diagnosis, 500);
|
|
2179
|
-
if (!["reused", "invalidated"].includes(settle.harnessDisposition)) throw new TypeError("Invalid harness disposition");
|
|
2180
|
-
args.push("--harness-disposition", settle.harnessDisposition);
|
|
2181
|
-
text("--cleanup-evidence", settle.cleanupEvidence, 500);
|
|
2182
|
-
text("--process-evidence", settle.processEvidence, 500);
|
|
2183
|
-
if (settle.remediationEvidence !== undefined) args.push("--remediation-evidence", settle.remediationEvidence);
|
|
2184
|
-
}
|
|
2185
|
-
revision("--remediates-evidence-revision", request.remediatesEvidenceRevision);
|
|
2186
|
-
args.push(...nativeUntrackedSelectionArguments(nativeUntrackedSelection(request)));
|
|
2187
|
-
const operation = NATIVE_REVIEW_OPERATION.SDD_ATTEMPT;
|
|
2188
|
-
const { body } = await this.negotiated(operation, request.workspaceRoot, args, true);
|
|
2189
|
-
return decode(operation, true, () => {
|
|
2190
|
-
const result = body as unknown as NativeSddAttemptResult;
|
|
2191
|
-
if (!result || !["proceed", "blocked", "complete"].includes(result.state)) throw new TypeError("Invalid compact state");
|
|
2192
|
-
if (verb === "acquire" && result.state === "proceed" && !isCanonicalProcessString(result.token)) throw new TypeError("Missing compact token");
|
|
2193
|
-
if (result.reason !== undefined && typeof result.reason !== "string") throw new TypeError("Invalid compact reason");
|
|
2194
|
-
return { state: result.state, ...(result.token === undefined ? {} : { token: result.token }), ...(result.reason === undefined ? {} : { reason: result.reason }) };
|
|
2195
|
-
});
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
2111
|
async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusV2> {
|
|
2199
2112
|
return this.sddProjection(request, false);
|
|
2200
2113
|
}
|