@aeondave/pi-persona 1.17.1 → 1.17.4
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/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
package/src/ui/model-picker.ts
CHANGED
|
@@ -5,12 +5,32 @@
|
|
|
5
5
|
* hundreds of models) and the viewport *follows the selection* — the highlighted
|
|
6
6
|
* row is always on screen, so picking never jumps off the top.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Machine-facing filtering/ordering remain separate from human fuzzy search.
|
|
9
|
+
* Only caller-supplied options are eligible; metadata never adds choices.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
12
|
+
import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
Container, CURSOR_MARKER, type Focusable, fuzzyFilter, getKeybindings, Input,
|
|
15
|
+
renderFakeCursor, setKeybindings, type TUI, truncateToWidth, visibleWidth,
|
|
16
|
+
} from "@earendil-works/pi-tui";
|
|
17
|
+
import { sanitizeTerminalText } from "./presentation.ts";
|
|
18
|
+
|
|
19
|
+
/** Structural subset of host snapshots; no registry lookup or availability policy here. */
|
|
20
|
+
export interface ModelPickerMetadata {
|
|
21
|
+
provider: string;
|
|
22
|
+
id: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
contextWindow?: number;
|
|
25
|
+
reasoning?: boolean;
|
|
26
|
+
input?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ModelPickerConfig {
|
|
30
|
+
currentRef?: string;
|
|
31
|
+
models?: readonly ModelPickerMetadata[];
|
|
32
|
+
keybindings?: KeybindingsManager;
|
|
33
|
+
}
|
|
14
34
|
|
|
15
35
|
const VIEWPORT = 12; // rows shown at once (the rest scrolls)
|
|
16
36
|
|
|
@@ -38,24 +58,48 @@ export function orderModelRefs(refs: string[], sessionProvider?: string): string
|
|
|
38
58
|
return [...refs].sort((a, b) => Number(!isSession(a)) - Number(!isSession(b)) || a.localeCompare(b));
|
|
39
59
|
}
|
|
40
60
|
|
|
41
|
-
/**
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
/** One safe display line; never use this to rewrite a returned model ref. */
|
|
62
|
+
function displayText(value: string): string {
|
|
63
|
+
return sanitizeTerminalText(value)
|
|
64
|
+
.replace(/[\n\r\t\u2028\u2029]/g, " ")
|
|
65
|
+
.replace(/[\u202a-\u202e\u2066-\u2069]/g, "");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function metadataText(model: ModelPickerMetadata | undefined): string {
|
|
69
|
+
if (!model) return "";
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
const context = model.contextWindow;
|
|
72
|
+
if (context !== undefined && Number.isSafeInteger(context) && context > 0) {
|
|
73
|
+
const size = context >= 1_000_000 ? `${Math.round(context / 100_000) / 10}m`
|
|
74
|
+
: context >= 1000 ? `${Math.round(context / 100) / 10}k` : String(context);
|
|
75
|
+
parts.push(`${size} ctx`);
|
|
76
|
+
}
|
|
77
|
+
if (model.reasoning === true) parts.push("reasoning");
|
|
78
|
+
if (model.input?.includes("image")) parts.push("vision");
|
|
79
|
+
// Functional metadata gets scarce columns before a potentially long/duplicated display name.
|
|
80
|
+
if (model.name && model.name !== model.id) parts.push(truncateToWidth(displayText(model.name), 32));
|
|
81
|
+
return parts.length ? ` · ${parts.join(" · ")}` : "";
|
|
46
82
|
}
|
|
47
83
|
|
|
48
|
-
export class ModelPicker extends Container {
|
|
84
|
+
export class ModelPicker extends Container implements Focusable {
|
|
49
85
|
private theme: Theme;
|
|
50
86
|
private title: string;
|
|
51
87
|
private options: string[];
|
|
52
88
|
private sessionProvider: string | undefined;
|
|
89
|
+
private currentRef: string | undefined;
|
|
53
90
|
private done: (ref: string | undefined) => void;
|
|
54
91
|
private tui: TUI;
|
|
55
|
-
private
|
|
92
|
+
private input: Input;
|
|
93
|
+
private keybindings: ReturnType<typeof getKeybindings>;
|
|
94
|
+
private models = new Map<string, ModelPickerMetadata>();
|
|
95
|
+
private list: string[];
|
|
56
96
|
private selected = 0;
|
|
57
97
|
private scroll = 0;
|
|
58
|
-
private
|
|
98
|
+
private pastePending = false;
|
|
99
|
+
private pasteTail = "";
|
|
100
|
+
|
|
101
|
+
get focused(): boolean { return this.input.focused; }
|
|
102
|
+
set focused(value: boolean) { this.input.focused = value; }
|
|
59
103
|
|
|
60
104
|
constructor(
|
|
61
105
|
tui: TUI,
|
|
@@ -64,97 +108,130 @@ export class ModelPicker extends Container {
|
|
|
64
108
|
options: string[],
|
|
65
109
|
sessionProvider: string | undefined,
|
|
66
110
|
done: (ref: string | undefined) => void,
|
|
111
|
+
config: ModelPickerConfig = {},
|
|
67
112
|
) {
|
|
68
113
|
super();
|
|
69
114
|
this.tui = tui;
|
|
70
115
|
this.theme = theme;
|
|
71
|
-
this.title = title;
|
|
72
|
-
this.options = options;
|
|
116
|
+
this.title = displayText(title);
|
|
117
|
+
this.options = [...options];
|
|
118
|
+
this.list = this.options;
|
|
73
119
|
this.sessionProvider = sessionProvider;
|
|
120
|
+
this.currentRef = config.currentRef;
|
|
74
121
|
this.done = done;
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
private filtered(): string[] {
|
|
84
|
-
return filterModels(this.options, this.query);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
override render(width: number): string[] {
|
|
88
|
-
this.lastWidth = width;
|
|
89
|
-
const inner = this.inner();
|
|
90
|
-
const b = (s: string): string => this.theme.fg("accent", s);
|
|
91
|
-
const framed = super.render(inner).map((line) => {
|
|
92
|
-
const pad = " ".repeat(Math.max(0, inner - visibleWidth(line)));
|
|
93
|
-
return `${b("│")} ${line}${pad} ${b("│")}`;
|
|
122
|
+
this.keybindings = config.keybindings ?? getKeybindings();
|
|
123
|
+
for (const model of config.models ?? []) this.models.set(`${model.provider}/${model.id}`, model);
|
|
124
|
+
this.selected = Math.max(0, this.list.indexOf(config.currentRef ?? ""));
|
|
125
|
+
this.input = new Input({
|
|
126
|
+
prompt: "",
|
|
127
|
+
placeholder: "Search name, id or provider",
|
|
128
|
+
placeholderStyle: (text) => this.theme.fg("dim", text),
|
|
94
129
|
});
|
|
95
|
-
|
|
130
|
+
this.addChild(this.input);
|
|
96
131
|
}
|
|
97
132
|
|
|
98
|
-
private
|
|
99
|
-
|
|
133
|
+
private search(): void {
|
|
134
|
+
const previous = this.list[this.selected];
|
|
135
|
+
const query = this.input.getValue().trim();
|
|
136
|
+
this.list = fuzzyFilter(this.options, query, (ref) =>
|
|
137
|
+
displayText(`${ref} ${this.models.get(ref)?.name ?? ""}`));
|
|
138
|
+
// Full qualified refs beat name/token scores, without changing the caller's order at rest.
|
|
139
|
+
if (query) {
|
|
140
|
+
const literal = this.list.indexOf(query);
|
|
141
|
+
const exact = literal >= 0 ? literal : this.list.findIndex((ref) => ref.toLowerCase() === query.toLowerCase());
|
|
142
|
+
if (exact >= 0) {
|
|
143
|
+
if (exact > 0) this.list.unshift(...this.list.splice(exact, 1));
|
|
144
|
+
this.selected = 0; // typing an exact ref must not confirm a surviving fuzzy sibling
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.selected = previous === undefined ? 0 : Math.max(0, this.list.indexOf(previous));
|
|
100
149
|
}
|
|
101
150
|
|
|
102
|
-
|
|
103
|
-
|
|
151
|
+
override render(width: number): string[] {
|
|
152
|
+
const outer = Math.max(0, Math.min(104, Math.floor(width)));
|
|
153
|
+
if (outer === 0) return [];
|
|
154
|
+
const framed = outer >= 8;
|
|
155
|
+
const inner = framed ? outer - 4 : outer;
|
|
104
156
|
const t = this.theme;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
const fit = (line: string): string => truncateToWidth(line, inner, "…");
|
|
158
|
+
let inputLine = this.input.render(inner)[0] ?? "";
|
|
159
|
+
// Native Input can overrun a one-column viewport with a wide cursor grapheme.
|
|
160
|
+
if (visibleWidth(inputLine) > inner) {
|
|
161
|
+
inputLine = `${this.focused ? CURSOR_MARKER : ""}${renderFakeCursor(" ")}`;
|
|
162
|
+
}
|
|
163
|
+
const lines = [
|
|
164
|
+
t.fg("accent", t.bold(fit(this.title))),
|
|
165
|
+
t.fg("dim", fit(`${this.list.length}/${this.options.length} · fuzzy search`)),
|
|
166
|
+
inputLine,
|
|
167
|
+
"",
|
|
168
|
+
];
|
|
169
|
+
this.scroll = visibleWindow(this.selected, VIEWPORT, this.list.length, this.scroll);
|
|
170
|
+
if (!this.list.length) {
|
|
171
|
+
lines.push(t.fg("dim", fit("(no model matches — edit search to widen)")));
|
|
118
172
|
} else {
|
|
119
|
-
const end = Math.min(list.length, this.scroll + VIEWPORT);
|
|
120
|
-
if (this.scroll > 0)
|
|
173
|
+
const end = Math.min(this.list.length, this.scroll + VIEWPORT);
|
|
174
|
+
if (this.scroll > 0) lines.push(t.fg("dim", fit(`▲ ${this.scroll} above`)));
|
|
121
175
|
for (let i = this.scroll; i < end; i++) {
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
this.
|
|
176
|
+
const ref = this.list[i]!;
|
|
177
|
+
const current = ref === this.currentRef ? "✓" : " ";
|
|
178
|
+
const star = this.sessionProvider && ref.startsWith(`${this.sessionProvider}/`) ? "★" : " ";
|
|
179
|
+
// Ref comes first; friendly name and bounded, explicit metadata spend only spare columns.
|
|
180
|
+
const row = fit(`${i === this.selected ? "▸" : " "} ${current}${star} ${displayText(ref)}${metadataText(this.models.get(ref))}`);
|
|
181
|
+
lines.push(i === this.selected ? t.fg("accent", row) : row);
|
|
127
182
|
}
|
|
128
|
-
if (end < list.length)
|
|
183
|
+
if (end < this.list.length) lines.push(t.fg("dim", fit(`▼ ${this.list.length - end} below`)));
|
|
129
184
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
const keys = (action: Parameters<typeof this.keybindings.getKeys>[0]): string => this.keybindings.getKeys(action).join("/");
|
|
186
|
+
lines.push("", t.fg("dim", fit("✓ current · ★ session provider")), t.fg("dim", fit(
|
|
187
|
+
`${keys("tui.select.up")}/${keys("tui.select.down")} move · ${keys("tui.select.confirm")} select · ${keys("tui.select.cancel")} keep default`,
|
|
188
|
+
)));
|
|
189
|
+
if (!framed) return lines;
|
|
190
|
+
const b = (text: string): string => t.fg("accent", text);
|
|
191
|
+
return [
|
|
192
|
+
b(`┌${"─".repeat(inner + 2)}┐`),
|
|
193
|
+
...lines.map((line) => `${b("│")} ${line}${" ".repeat(Math.max(0, inner - visibleWidth(line)))} ${b("│")}`),
|
|
194
|
+
b(`└${"─".repeat(inner + 2)}┘`),
|
|
195
|
+
];
|
|
133
196
|
}
|
|
134
197
|
|
|
135
198
|
handleInput(keyData: string): void {
|
|
136
|
-
const kb =
|
|
137
|
-
|
|
138
|
-
|
|
199
|
+
const kb = this.keybindings;
|
|
200
|
+
// While Input buffers a bracketed paste, even a standalone Enter/Escape
|
|
201
|
+
// chunk is text, not a picker action. Retain only enough tail to detect
|
|
202
|
+
// an end marker split across chunks; Input owns the actual paste buffer.
|
|
203
|
+
const pasteData = this.pasteTail + keyData;
|
|
204
|
+
const editingPaste = this.pastePending || keyData.includes("\x1b[200~");
|
|
205
|
+
this.pastePending = editingPaste && !pasteData.includes("\x1b[201~");
|
|
206
|
+
this.pasteTail = this.pastePending ? pasteData.slice(-5) : "";
|
|
207
|
+
if (!editingPaste && kb.matches(keyData, "tui.select.up")) {
|
|
139
208
|
this.selected = Math.max(0, this.selected - 1);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
209
|
+
} else if (!editingPaste && kb.matches(keyData, "tui.select.down")) {
|
|
210
|
+
this.selected = Math.min(Math.max(0, this.list.length - 1), this.selected + 1);
|
|
211
|
+
} else if (!editingPaste && kb.matches(keyData, "tui.select.confirm")) {
|
|
212
|
+
const ref = this.list[this.selected];
|
|
213
|
+
if (ref !== undefined) this.done(ref);
|
|
214
|
+
return;
|
|
215
|
+
} else if (!editingPaste && kb.matches(keyData, "tui.select.cancel")) {
|
|
147
216
|
this.done(undefined);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
217
|
+
return;
|
|
218
|
+
} else {
|
|
219
|
+
const before = this.input.getValue();
|
|
220
|
+
// Input has no per-instance keybindings API (Pi 1.4). Scope its global
|
|
221
|
+
// lookup to this synchronous call and restore it even if handling throws.
|
|
222
|
+
const global = getKeybindings();
|
|
223
|
+
try {
|
|
224
|
+
if (global !== kb) setKeybindings(kb);
|
|
225
|
+
this.input.handleInput(keyData);
|
|
226
|
+
} finally {
|
|
227
|
+
if (global !== kb) setKeybindings(global);
|
|
153
228
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
229
|
+
const value = this.input.getValue();
|
|
230
|
+
const safe = displayText(value);
|
|
231
|
+
if (safe !== value) this.input.setValue(safe);
|
|
232
|
+
if (before !== safe) this.search();
|
|
158
233
|
}
|
|
234
|
+
this.invalidate();
|
|
235
|
+
this.tui.requestRender();
|
|
159
236
|
}
|
|
160
237
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Persisted orchestration launch requests. Rendering never consults live persona or model settings. */
|
|
2
|
+
import { resolveLegThinking } from "../core/leg-thinking.ts";
|
|
3
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
4
|
+
import type { ModelParticipant, ModelSource } from "../persona/model-participants.ts";
|
|
5
|
+
import { compactInlineText, sanitizeTerminalText } from "./presentation.ts";
|
|
6
|
+
|
|
7
|
+
export interface OrchestrationLaunchParticipant {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
agent: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
modelSource: ModelSource;
|
|
13
|
+
thinkingRequested?: ThinkingLevel;
|
|
14
|
+
setupPending: boolean;
|
|
15
|
+
param?: string;
|
|
16
|
+
phase?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OrchestrationLaunchSnapshot {
|
|
20
|
+
participants: OrchestrationLaunchParticipant[];
|
|
21
|
+
unavailable?: true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function snapshotOrchestrationParticipants(
|
|
25
|
+
participants: readonly ModelParticipant[],
|
|
26
|
+
opts: { agentThinking(agent: string): ThinkingLevel | undefined; childThinking: unknown; pendingKeys: ReadonlySet<string> },
|
|
27
|
+
): OrchestrationLaunchSnapshot {
|
|
28
|
+
return { participants: participants.map((p) => {
|
|
29
|
+
const agentThinking = opts.agentThinking(p.agent);
|
|
30
|
+
const thinkingRequested = resolveLegThinking(p, agentThinking ? { thinking: agentThinking } : {}, opts.childThinking);
|
|
31
|
+
return { key: p.key, label: p.label, agent: p.agent, modelSource: p.source, setupPending: !p.assigned && opts.pendingKeys.has(p.key),
|
|
32
|
+
...(p.model ? { model: p.model } : {}), ...(thinkingRequested ? { thinkingRequested } : {}), ...(p.param ? { param: p.param } : {}) };
|
|
33
|
+
}) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Three individually bounded rows collapsed; expansion retains every participant and full model ref. */
|
|
37
|
+
export function orchestrationLaunchLines(snapshot: OrchestrationLaunchSnapshot | undefined, expanded: boolean): string[] {
|
|
38
|
+
if (!snapshot || snapshot.unavailable) return ["Launch metadata unavailable"];
|
|
39
|
+
const rows = expanded ? snapshot.participants : snapshot.participants.slice(0, 3);
|
|
40
|
+
const lines = rows.map((p) => {
|
|
41
|
+
const clean = (value: string, maxChars: number): string => expanded ? sanitizeTerminalText(value) : compactInlineText(value, { maxChars });
|
|
42
|
+
const label = clean(`${p.phase ? `${p.phase} / ` : ""}${p.label}${p.param ? ` [${p.param}]` : ""}`, 48);
|
|
43
|
+
const model = p.model ? clean(p.model, 64) : "unknown";
|
|
44
|
+
const modelText = p.setupPending ? `model pending (current default ${model}; setup may change)` : `requested model ${model}`;
|
|
45
|
+
return ` ${label} · ${modelText} · requested effort ${p.thinkingRequested ?? "unknown"}`;
|
|
46
|
+
});
|
|
47
|
+
if (!expanded && snapshot.participants.length > rows.length) lines.push(` +${snapshot.participants.length - rows.length} more · expand for launch details`);
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
@@ -225,8 +225,14 @@ export class PersonaSettingsPanel implements Component {
|
|
|
225
225
|
const title = this.theme.fg("accent", this.theme.bold("Persona settings"));
|
|
226
226
|
const file = fitLine(this.theme.fg("dim", `Configuration file: ${compactInlineText(this.data.file, { maxChars: 240 })}`), pageWidth);
|
|
227
227
|
const remembered = fitLine(this.theme.fg("dim", `Remembered persona: ${this.data.lastPersona ? compactInlineText(this.data.lastPersona, { maxChars: 120 }) : "none"}`), pageWidth);
|
|
228
|
+
// Show the configured primary binding; listing every alias would hide close/edit on narrow screens.
|
|
229
|
+
const keys = (action: "tui.select.up" | "tui.select.down" | "tui.select.confirm" | "tui.select.cancel"): string =>
|
|
230
|
+
this.keybindings.getKeys(action).slice(0, 1).map((key) => key === "escape" ? "esc" : key).join("/") || "unbound";
|
|
231
|
+
const move = `${keys("tui.select.up")}/${keys("tui.select.down")}`;
|
|
232
|
+
const confirm = keys("tui.select.confirm");
|
|
233
|
+
const cancel = keys("tui.select.cancel");
|
|
228
234
|
const filter = this.searchMode
|
|
229
|
-
? this.theme.fg("accent", `Filter (type, backspace,
|
|
235
|
+
? this.theme.fg("accent", `Filter (type, backspace, ${confirm}): ${compactInlineText(this.query, { maxChars: 120 })}▌`)
|
|
230
236
|
: this.query
|
|
231
237
|
? this.theme.fg("dim", `Filter: ${compactInlineText(this.query, { maxChars: 120 })}`)
|
|
232
238
|
: this.theme.fg("dim", "Filter: press / to search");
|
|
@@ -234,8 +240,8 @@ export class PersonaSettingsPanel implements Component {
|
|
|
234
240
|
const footer = [
|
|
235
241
|
...(this.data.notice ? [fitLine(this.theme.fg("warning", compactInlineText(this.data.notice, { maxChars: 200 })), pageWidth)] : []),
|
|
236
242
|
...(pageWidth < 76
|
|
237
|
-
? [
|
|
238
|
-
: [
|
|
243
|
+
? [`${move} move ${confirm} edit r reset`, `/ filter m models d info ${cancel} close`]
|
|
244
|
+
: [`${move} move ${confirm} edit r reset / filter m models d diagnostics ${cancel} close`])
|
|
239
245
|
.map((hint) => fitLine(this.theme.fg("dim", hint), pageWidth)),
|
|
240
246
|
];
|
|
241
247
|
const bodyHeight = Math.max(1, terminalRows - header.length - footer.length);
|
package/src/ui/presentation.ts
CHANGED
|
@@ -168,3 +168,27 @@ export function compactVisibleText(
|
|
|
168
168
|
originalChars: input.length,
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The prompt an operator reads before releasing a flow checkpoint: which gate, what runs next,
|
|
174
|
+
* and enough of the gated phase's output to decide. The prompt is modal, so it cannot point at an
|
|
175
|
+
* "expand" key; it states how much it left out instead.
|
|
176
|
+
*/
|
|
177
|
+
export function gateReviewText(gate: {
|
|
178
|
+
flow: string;
|
|
179
|
+
phase: string;
|
|
180
|
+
/** The phases the approval releases. */
|
|
181
|
+
next: ReadonlyArray<{ id: string; strategy: string }>;
|
|
182
|
+
output: string;
|
|
183
|
+
}): string {
|
|
184
|
+
const name = (value: string): string => compactInlineText(value, { maxChars: 64 });
|
|
185
|
+
const body = compactVisibleText(gate.output, { maxLines: 12, maxLineChars: 100 });
|
|
186
|
+
return [
|
|
187
|
+
`Checkpoint "${name(gate.phase)}" of flow "${name(gate.flow)}" — approve and continue?`,
|
|
188
|
+
...(gate.next.length > 0 ? [`then: ${gate.next.map((p) => `${name(p.id)} (${name(p.strategy)})`).join(", ")}`] : []),
|
|
189
|
+
// Everything below is the child's text: a fixed rule keeps it from passing as more prompt.
|
|
190
|
+
"--- gated output (untrusted) ---",
|
|
191
|
+
body.text,
|
|
192
|
+
...(body.truncated ? [`(+${Math.max(0, gate.output.length - body.text.length)} characters not shown)`] : []),
|
|
193
|
+
].join("\n");
|
|
194
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** UI-only prose projection. Never rewrites stored/model-facing text or unwraps trust fences. */
|
|
2
|
+
import { getMarkdownTheme, type Theme, type ThemeBg } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Markdown, Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { sanitizeTerminalText } from "./presentation.ts";
|
|
5
|
+
|
|
6
|
+
interface ReportBodyOptions {
|
|
7
|
+
/** Trusted chrome stays literal Text, outside the Markdown body. Evaluated at paint time. */
|
|
8
|
+
header?: (width: number) => string;
|
|
9
|
+
/** Custom session cards only. Normal tools already have the host's shell. */
|
|
10
|
+
background?: ThemeBg;
|
|
11
|
+
padding?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ReportBody implements Component {
|
|
15
|
+
private readonly markdownTheme = getMarkdownTheme();
|
|
16
|
+
private readonly markdown: Markdown;
|
|
17
|
+
readonly source: string;
|
|
18
|
+
private theme: Theme;
|
|
19
|
+
private options: ReportBodyOptions;
|
|
20
|
+
constructor(source: string, theme: Theme, options: ReportBodyOptions) {
|
|
21
|
+
this.source = source;
|
|
22
|
+
this.theme = theme;
|
|
23
|
+
this.options = options;
|
|
24
|
+
// No defaultTextStyle: Pi caches its default style prefix independently of invalidate().
|
|
25
|
+
// Apply the fallback foreground/background outside Markdown so theme changes stay fresh.
|
|
26
|
+
this.markdown = new Markdown(source, 0, 0, this.markdownTheme);
|
|
27
|
+
}
|
|
28
|
+
update(theme: Theme, options: ReportBodyOptions): void {
|
|
29
|
+
this.theme = theme;
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.invalidate();
|
|
32
|
+
}
|
|
33
|
+
invalidate(): void {
|
|
34
|
+
Object.assign(this.markdownTheme, getMarkdownTheme());
|
|
35
|
+
// Native Markdown retains its source-token cache while clearing themed/layout output.
|
|
36
|
+
this.markdown.invalidate();
|
|
37
|
+
}
|
|
38
|
+
render(width: number): string[] {
|
|
39
|
+
if (width <= 0) return [];
|
|
40
|
+
const padding = Math.min(Math.max(0, Math.floor(this.options.padding ?? 0)), Math.max(0, Math.floor((width - 1) / 2)));
|
|
41
|
+
const innerWidth = Math.max(1, width - 2 * padding);
|
|
42
|
+
const header = this.options.header?.(innerWidth);
|
|
43
|
+
const lines = [
|
|
44
|
+
...(header ? new Text(header, 0, 0).render(innerWidth) : []),
|
|
45
|
+
...this.markdown.render(innerWidth).map((line) => this.theme.fg("toolOutput", line)),
|
|
46
|
+
];
|
|
47
|
+
const background = this.options.background;
|
|
48
|
+
return new Text(lines.join("\n"), padding, 0, background ? (text) => this.theme.bg(background, text) : undefined)
|
|
49
|
+
.render(width).map((line) => truncateToWidth(line, width));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Keep one native Markdown instance per report. Width changes reflow natively; invalidate() refreshes
|
|
54
|
+
* theme/layout, not source tokens. Tool callbacks can also reuse Pi's lastComponent across redraws.
|
|
55
|
+
* No global transcript/content cache: old reports are reclaimed with their host components. */
|
|
56
|
+
export function createReportBody(text: string, theme: Theme, options: ReportBodyOptions = {}, previous?: Component): Component {
|
|
57
|
+
const source = sanitizeTerminalText(text);
|
|
58
|
+
if (previous instanceof ReportBody && previous.source === source) {
|
|
59
|
+
previous.update(theme, options);
|
|
60
|
+
return previous;
|
|
61
|
+
}
|
|
62
|
+
return new ReportBody(source, theme, options);
|
|
63
|
+
}
|