pi-advisor-flow 0.5.0 → 0.5.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 +54 -0
- package/README.md +37 -71
- package/extensions/index.ts +7 -7
- package/package.json +19 -6
- package/src/commands/activation-preparation.ts +117 -0
- package/src/commands/activation.ts +135 -0
- package/src/commands/lifecycle.ts +83 -0
- package/src/commands/manual-command.ts +107 -0
- package/src/commands/manual-consultation.ts +144 -0
- package/src/commands/manual-progress.ts +93 -0
- package/src/commands/model-commands.ts +66 -0
- package/src/commands/model-options.ts +181 -0
- package/src/commands/model-picker.ts +110 -0
- package/src/commands/registration.ts +27 -0
- package/src/commands/renderers.ts +76 -0
- package/src/commands/runtime.ts +114 -0
- package/src/commands/settings-commands.ts +80 -0
- package/src/commands/settings-persistence.ts +81 -0
- package/src/commands/types.ts +78 -0
- package/src/commands.ts +9 -946
- package/src/config/args.ts +37 -0
- package/src/config/defaults.ts +190 -0
- package/src/config/state.ts +199 -0
- package/src/config/storage.ts +224 -0
- package/src/config/types.ts +63 -0
- package/src/config/validation.ts +234 -0
- package/src/config.ts +104 -791
- package/src/conversation.ts +4 -2
- package/src/herdr.ts +1 -1
- package/src/model-stream.ts +25 -2
- package/src/scout-context.ts +2 -2
- package/src/scout.ts +1 -1
- package/src/tools/consultation.ts +337 -0
- package/src/tools/gate-policy.ts +132 -0
- package/src/tools/gate-protocol.ts +127 -0
- package/src/tools/loop-gate.ts +210 -0
- package/src/tools/prompts.ts +159 -0
- package/src/tools/register-ask-advisor.ts +225 -0
- package/src/tools/register-lifecycle.ts +89 -0
- package/src/tools/register-outcome.ts +75 -0
- package/src/tools/register-renderers.ts +88 -0
- package/src/tools/registration.ts +35 -0
- package/src/tools/render-advisor-result.ts +168 -0
- package/src/tools/render-common.ts +120 -0
- package/src/tools/scout-status.ts +206 -0
- package/src/tools/session.ts +3 -0
- package/src/tools/types.ts +119 -0
- package/src/tools.ts +42 -1811
- package/src/ui/manual-dialog-render.ts +150 -0
- package/src/ui/manual-dialog.ts +305 -0
- package/src/ui/model-selector.ts +166 -0
- package/src/ui/settings-formatting.ts +133 -0
- package/src/ui/settings-items.ts +323 -0
- package/src/ui/settings-list-adapter.ts +103 -0
- package/src/ui/settings-mutations.ts +80 -0
- package/src/ui/settings-selector.ts +161 -0
- package/src/ui/text-setting-submenu.ts +66 -0
- package/src/ui/types.ts +111 -0
- package/src/ui.ts +9 -1418
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { advisorOutcomeLoggingRef } from "../config/state.js";
|
|
4
|
+
import { loadConfig } from "../config/storage.js";
|
|
5
|
+
import { textFrom } from "../conversation.js";
|
|
6
|
+
import { ADOPTIONS, VALIDATIONS } from "../outcomes.js";
|
|
7
|
+
import type { ToolRegistrationContext } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const registerOutcomeTool = ({
|
|
10
|
+
appendOutcome: appendAdvisorOutcome,
|
|
11
|
+
pi,
|
|
12
|
+
session,
|
|
13
|
+
}: ToolRegistrationContext): void => {
|
|
14
|
+
pi.registerTool({
|
|
15
|
+
description:
|
|
16
|
+
"Voluntarily record the settled adoption and validation outcome for a displayed adviceId when global outcome logging is enabled.",
|
|
17
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
18
|
+
loadConfig(ctx);
|
|
19
|
+
if (!advisorOutcomeLoggingRef) {
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{ text: "Outcome logging is disabled globally.", type: "text" },
|
|
23
|
+
],
|
|
24
|
+
details: { recorded: false },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const advice = session.reserveAdvice(params.adviceId);
|
|
28
|
+
if (!advice) {
|
|
29
|
+
throw new Error("Unknown, already recorded, or pending adviceId.");
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await appendAdvisorOutcome({
|
|
33
|
+
adoption: params.adoption as (typeof ADOPTIONS)[number],
|
|
34
|
+
advice: advice.advice,
|
|
35
|
+
trigger: advice.trigger,
|
|
36
|
+
validationStatus:
|
|
37
|
+
params.validationStatus as (typeof VALIDATIONS)[number],
|
|
38
|
+
});
|
|
39
|
+
session.commitAdvice(params.adviceId);
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{ text: "Advisor outcome recorded locally.", type: "text" },
|
|
43
|
+
],
|
|
44
|
+
details: { recorded: true },
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
session.releaseAdvice(params.adviceId);
|
|
48
|
+
if (ctx.hasUI) {
|
|
49
|
+
ctx.ui.notify(
|
|
50
|
+
"Advisor outcome could not be recorded locally.",
|
|
51
|
+
"warning"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
text: "Advisor outcome was not recorded; Advisor execution remains usable.",
|
|
58
|
+
type: "text",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
details: { recorded: false },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
label: "Record Advisor Outcome",
|
|
66
|
+
name: "record_advisor_outcome",
|
|
67
|
+
parameters: Type.Object({
|
|
68
|
+
adoption: Type.String({ enum: ADOPTIONS }),
|
|
69
|
+
adviceId: Type.String(),
|
|
70
|
+
validationStatus: Type.String({ enum: VALIDATIONS }),
|
|
71
|
+
}),
|
|
72
|
+
renderCall: () => new Text("[advisor] Record outcome", 0, 0),
|
|
73
|
+
renderResult: (result) => new Text(textFrom(result.content), 0, 0),
|
|
74
|
+
});
|
|
75
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
getMarkdownTheme,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
6
|
+
import { getAdvisorSettings } from "../config/state.js";
|
|
7
|
+
import { formatAdvisorUsage } from "../usage.js";
|
|
8
|
+
import { adviceForDisplay, renderAdvisorCallBox } from "./render-common.js";
|
|
9
|
+
import { renderScoutDetails } from "./scout-status.js";
|
|
10
|
+
import type { GateDecision, ScoutToolDetails } from "./types.js";
|
|
11
|
+
|
|
12
|
+
export const registerToolRenderers = (pi: ExtensionAPI): void => {
|
|
13
|
+
pi.registerEntryRenderer?.(
|
|
14
|
+
"advisor-scout-result",
|
|
15
|
+
(entry, { expanded }, theme) => {
|
|
16
|
+
const scout = entry.data as ScoutToolDetails;
|
|
17
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
18
|
+
renderScoutDetails(box, scout, Boolean(expanded), theme);
|
|
19
|
+
return box;
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
pi.registerMessageRenderer?.(
|
|
24
|
+
"advisor-loop-call",
|
|
25
|
+
(message, _options, theme) => {
|
|
26
|
+
const details = message.details as { question?: string } | undefined;
|
|
27
|
+
return renderAdvisorCallBox(details?.question, theme);
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
pi.registerMessageRenderer?.(
|
|
32
|
+
"advisor-loop-result",
|
|
33
|
+
(message, { expanded }, theme) => {
|
|
34
|
+
const details = message.details as
|
|
35
|
+
| {
|
|
36
|
+
advisor?: string;
|
|
37
|
+
decision?: GateDecision;
|
|
38
|
+
text?: string;
|
|
39
|
+
usage?: unknown;
|
|
40
|
+
}
|
|
41
|
+
| undefined;
|
|
42
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
43
|
+
box.addChild(
|
|
44
|
+
new Text(
|
|
45
|
+
theme.fg(
|
|
46
|
+
"warning",
|
|
47
|
+
theme.bold(`◆ ADVISOR GATE: ${details?.decision ?? "failure"}`)
|
|
48
|
+
),
|
|
49
|
+
0,
|
|
50
|
+
0
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
if (details?.advisor) {
|
|
54
|
+
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
55
|
+
}
|
|
56
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
57
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
58
|
+
if (usage) {
|
|
59
|
+
box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (details?.text) {
|
|
63
|
+
box.addChild(
|
|
64
|
+
new Markdown(
|
|
65
|
+
adviceForDisplay(details.text, Boolean(expanded)),
|
|
66
|
+
0,
|
|
67
|
+
0,
|
|
68
|
+
getMarkdownTheme()
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
box.addChild(
|
|
73
|
+
new Text(
|
|
74
|
+
theme.fg(
|
|
75
|
+
"error",
|
|
76
|
+
typeof message.content === "string"
|
|
77
|
+
? message.content
|
|
78
|
+
: "Advisor gate failed."
|
|
79
|
+
),
|
|
80
|
+
0,
|
|
81
|
+
0
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return box;
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { appendOutcome } from "../outcomes.js";
|
|
3
|
+
import type { AdvisorSessionState } from "../session-state.js";
|
|
4
|
+
import { consultAdvisor, runAdvisorGate } from "./consultation.js";
|
|
5
|
+
import { registerAskAdvisorTool } from "./register-ask-advisor.js";
|
|
6
|
+
import { registerToolLifecycle } from "./register-lifecycle.js";
|
|
7
|
+
import { registerOutcomeTool } from "./register-outcome.js";
|
|
8
|
+
import { registerToolRenderers } from "./register-renderers.js";
|
|
9
|
+
import { ScoutStatusManager } from "./scout-status.js";
|
|
10
|
+
import { advisorSessionState } from "./session.js";
|
|
11
|
+
import type {
|
|
12
|
+
ToolRegistrationContext,
|
|
13
|
+
ToolRegistrationDependencies,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
export const registerAdvisorTool = (
|
|
17
|
+
pi: ExtensionAPI,
|
|
18
|
+
session: AdvisorSessionState = advisorSessionState,
|
|
19
|
+
dependencies: ToolRegistrationDependencies = {}
|
|
20
|
+
): void => {
|
|
21
|
+
const registration: ToolRegistrationContext = {
|
|
22
|
+
appendOutcome: dependencies.appendOutcome ?? appendOutcome,
|
|
23
|
+
consult: dependencies.consult ?? consultAdvisor,
|
|
24
|
+
pi,
|
|
25
|
+
reservedCalls: new Set<string>(),
|
|
26
|
+
runGate: dependencies.runGate ?? runAdvisorGate,
|
|
27
|
+
scoutStatus: dependencies.statusManager ?? new ScoutStatusManager(),
|
|
28
|
+
session,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
registerToolRenderers(pi);
|
|
32
|
+
registerToolLifecycle(registration);
|
|
33
|
+
registerAskAdvisorTool(registration);
|
|
34
|
+
registerOutcomeTool(registration);
|
|
35
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
getMarkdownTheme,
|
|
4
|
+
type Theme,
|
|
5
|
+
type ToolRenderResultOptions,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { getAdvisorSettings } from "../config/state.js";
|
|
9
|
+
import { textFrom } from "../conversation.js";
|
|
10
|
+
import { formatAdvisorUsage } from "../usage.js";
|
|
11
|
+
import {
|
|
12
|
+
adviceForDisplay,
|
|
13
|
+
hasSoundVerdict,
|
|
14
|
+
renderAdvisorResponseHeader,
|
|
15
|
+
renderThinkingMarkdown,
|
|
16
|
+
SPINNER_FRAMES,
|
|
17
|
+
} from "./render-common.js";
|
|
18
|
+
import { renderScoutDetails } from "./scout-status.js";
|
|
19
|
+
import type { AdvisorToolContext, AdvisorToolDetails } from "./types.js";
|
|
20
|
+
|
|
21
|
+
const advisorResultDetails = (result: AgentToolResult<AdvisorToolDetails>) =>
|
|
22
|
+
result.details;
|
|
23
|
+
|
|
24
|
+
const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
|
|
25
|
+
if (context.state.phase !== phase && context.state.timerId) {
|
|
26
|
+
clearInterval(context.state.timerId);
|
|
27
|
+
context.state.timerId = undefined;
|
|
28
|
+
}
|
|
29
|
+
context.state.phase = phase;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const renderPartialAdvisorResult = (
|
|
33
|
+
box: Box,
|
|
34
|
+
result: AgentToolResult<AdvisorToolDetails>,
|
|
35
|
+
expanded: boolean,
|
|
36
|
+
theme: Theme,
|
|
37
|
+
context: AdvisorToolContext
|
|
38
|
+
) => {
|
|
39
|
+
const details = advisorResultDetails(result);
|
|
40
|
+
if (details?.scout) {
|
|
41
|
+
context.state.scout = details.scout;
|
|
42
|
+
}
|
|
43
|
+
const scout = details?.scout ?? context.state.scout;
|
|
44
|
+
const scoutActive =
|
|
45
|
+
scout?.status === "calling" || scout?.status === "streaming";
|
|
46
|
+
syncRenderPhase(context, scoutActive ? "scout" : "advisor");
|
|
47
|
+
if (!context.state.timerId) {
|
|
48
|
+
context.state.timerId = setInterval(() => context.invalidate(), 80);
|
|
49
|
+
}
|
|
50
|
+
if (scout) {
|
|
51
|
+
renderScoutDetails(box, scout, expanded, theme);
|
|
52
|
+
}
|
|
53
|
+
if (scoutActive || scout?.status === "cancelled") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const frame =
|
|
57
|
+
SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
|
|
58
|
+
const lines = [
|
|
59
|
+
`${theme.fg("warning", theme.bold(`◆ ADVISOR ${frame}`))} ${theme.fg("dim", "· Working…")}`,
|
|
60
|
+
];
|
|
61
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
62
|
+
if (details?.thinking?.trim()) {
|
|
63
|
+
const thought =
|
|
64
|
+
details.thinking.length > 200
|
|
65
|
+
? details.thinking.slice(-200)
|
|
66
|
+
: details.thinking;
|
|
67
|
+
box.addChild(renderThinkingMarkdown(thought, theme));
|
|
68
|
+
}
|
|
69
|
+
if (details?.text) {
|
|
70
|
+
box.addChild(
|
|
71
|
+
new Markdown(
|
|
72
|
+
adviceForDisplay(details.text, expanded),
|
|
73
|
+
0,
|
|
74
|
+
0,
|
|
75
|
+
getMarkdownTheme()
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const renderFinalAdvisorResult = (
|
|
82
|
+
box: Box,
|
|
83
|
+
result: AgentToolResult<AdvisorToolDetails>,
|
|
84
|
+
expanded: boolean,
|
|
85
|
+
theme: Theme,
|
|
86
|
+
context: AdvisorToolContext
|
|
87
|
+
) => {
|
|
88
|
+
syncRenderPhase(context, "final");
|
|
89
|
+
if (context.state.timerId) {
|
|
90
|
+
clearInterval(context.state.timerId);
|
|
91
|
+
context.state.timerId = undefined;
|
|
92
|
+
}
|
|
93
|
+
const details = advisorResultDetails(result);
|
|
94
|
+
if (details?.scout) {
|
|
95
|
+
context.state.scout = details.scout;
|
|
96
|
+
}
|
|
97
|
+
const scout = details?.scout ?? context.state.scout;
|
|
98
|
+
if (scout) {
|
|
99
|
+
renderScoutDetails(box, scout, expanded, theme);
|
|
100
|
+
}
|
|
101
|
+
if (scout?.status === "cancelled") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const advice = details?.text || textFrom(result.content);
|
|
105
|
+
const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
|
|
106
|
+
if (details?.advisor) {
|
|
107
|
+
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
108
|
+
}
|
|
109
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
110
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
111
|
+
if (usage) {
|
|
112
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const attachments = [
|
|
116
|
+
details?.draftBytes
|
|
117
|
+
? `Draft attached · ${details.draftBytes} B`
|
|
118
|
+
: undefined,
|
|
119
|
+
details?.preferenceBytes
|
|
120
|
+
? `Project preferences attached · ${details.preferenceBytes} B`
|
|
121
|
+
: undefined,
|
|
122
|
+
details?.trackedBytes
|
|
123
|
+
? `Tracked files attached · ${details.trackedBytes} B`
|
|
124
|
+
: undefined,
|
|
125
|
+
details?.untrackedBytes
|
|
126
|
+
? `Untracked files attached · ${details.untrackedBytes} B`
|
|
127
|
+
: undefined,
|
|
128
|
+
].filter(Boolean);
|
|
129
|
+
if (attachments.length) {
|
|
130
|
+
lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
|
|
131
|
+
}
|
|
132
|
+
const thinking = details?.thinking?.trim()
|
|
133
|
+
? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}`
|
|
134
|
+
: "";
|
|
135
|
+
const displayAdvice = advice || "(Advisor returned no advice.)";
|
|
136
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
137
|
+
if (thinking) {
|
|
138
|
+
box.addChild(renderThinkingMarkdown(thinking, theme));
|
|
139
|
+
}
|
|
140
|
+
box.addChild(
|
|
141
|
+
new Markdown(
|
|
142
|
+
adviceForDisplay(displayAdvice, expanded),
|
|
143
|
+
0,
|
|
144
|
+
0,
|
|
145
|
+
getMarkdownTheme()
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const renderAdvisorResult = (
|
|
151
|
+
result: AgentToolResult<AdvisorToolDetails>,
|
|
152
|
+
{ isPartial, expanded }: ToolRenderResultOptions,
|
|
153
|
+
theme: Theme,
|
|
154
|
+
context: AdvisorToolContext
|
|
155
|
+
) => {
|
|
156
|
+
const box =
|
|
157
|
+
context.lastComponent instanceof Box
|
|
158
|
+
? context.lastComponent
|
|
159
|
+
: new Box(1, 1, (text: string) => theme.bg("customMessageBg", text));
|
|
160
|
+
box.setBgFn((text) => theme.bg("customMessageBg", text));
|
|
161
|
+
box.clear();
|
|
162
|
+
if (isPartial) {
|
|
163
|
+
renderPartialAdvisorResult(box, result, expanded, theme, context);
|
|
164
|
+
} else {
|
|
165
|
+
renderFinalAdvisorResult(box, result, expanded, theme, context);
|
|
166
|
+
}
|
|
167
|
+
return box;
|
|
168
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
type Component,
|
|
5
|
+
Markdown,
|
|
6
|
+
Text,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import { advisorCollapseResponsesRef } from "../config/state.js";
|
|
11
|
+
|
|
12
|
+
export const SPINNER_FRAMES = [
|
|
13
|
+
"⠋",
|
|
14
|
+
"⠙",
|
|
15
|
+
"⠹",
|
|
16
|
+
"⠸",
|
|
17
|
+
"⠼",
|
|
18
|
+
"⠴",
|
|
19
|
+
"⠦",
|
|
20
|
+
"⠧",
|
|
21
|
+
"⠇",
|
|
22
|
+
"⠏",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const THINKING_PREFIX = " 💭 ";
|
|
26
|
+
const THINKING_PREFIX_WIDTH = visibleWidth(THINKING_PREFIX);
|
|
27
|
+
export type ThinkingTheme = Pick<Theme, "fg">;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Renders visible nested-model thinking with the same Markdown semantics as
|
|
31
|
+
* Pi's assistant thinking blocks while keeping the compact speech-bubble cue.
|
|
32
|
+
* The prefix is added after Markdown parsing so it cannot change block syntax.
|
|
33
|
+
*
|
|
34
|
+
* Note: During streaming, incomplete Markdown (e.g., `**text` without closing **)
|
|
35
|
+
* displays raw markers transiently until delimiters arrive. This mirrors expected
|
|
36
|
+
* behavior when typing Markdown incrementally and is acceptable in the context
|
|
37
|
+
* of brief thinking previews. When thinking is complete, all markers render.
|
|
38
|
+
*/
|
|
39
|
+
class ThinkingMarkdown implements Component {
|
|
40
|
+
private readonly markdown: Markdown;
|
|
41
|
+
private readonly prefix: string;
|
|
42
|
+
|
|
43
|
+
constructor(thinking: string, theme: ThinkingTheme) {
|
|
44
|
+
this.markdown = new Markdown(thinking.trim(), 0, 0, getMarkdownTheme(), {
|
|
45
|
+
color: (text) => theme.fg("thinkingText", text),
|
|
46
|
+
italic: true,
|
|
47
|
+
});
|
|
48
|
+
this.prefix = theme.fg("thinkingText", THINKING_PREFIX);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(width: number): string[] {
|
|
52
|
+
const renderWidth = Math.max(1, Math.floor(width));
|
|
53
|
+
const contentWidth = Math.max(1, renderWidth - THINKING_PREFIX_WIDTH);
|
|
54
|
+
const lines = this.markdown.render(contentWidth);
|
|
55
|
+
return lines.map((line, index) =>
|
|
56
|
+
truncateToWidth(
|
|
57
|
+
index === 0 ? `${this.prefix}${line}` : line,
|
|
58
|
+
renderWidth,
|
|
59
|
+
""
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
invalidate(): void {
|
|
65
|
+
this.markdown.invalidate();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const renderThinkingMarkdown = (
|
|
70
|
+
thinking: string,
|
|
71
|
+
theme: ThinkingTheme
|
|
72
|
+
): Component => new ThinkingMarkdown(thinking, theme);
|
|
73
|
+
|
|
74
|
+
export const resolveAdvisorRequest = (question?: string) =>
|
|
75
|
+
question?.trim() || undefined;
|
|
76
|
+
|
|
77
|
+
export const renderAdvisorCallBox = (
|
|
78
|
+
question: string | undefined,
|
|
79
|
+
theme: Theme
|
|
80
|
+
) => {
|
|
81
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
82
|
+
const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
|
|
83
|
+
const title = theme.fg("customMessageText", "Executor → Advisor");
|
|
84
|
+
box.addChild(
|
|
85
|
+
new Text(
|
|
86
|
+
question
|
|
87
|
+
? `${label} ${title}\n${theme.fg("dim", ` ${question}`)}`
|
|
88
|
+
: `${label} ${title}`,
|
|
89
|
+
0,
|
|
90
|
+
0
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
return box;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const COLLAPSED_ADVICE_LINES = 12;
|
|
97
|
+
// The system prompt requires this exact first line, so the match is exact too.
|
|
98
|
+
const SOUND_VERDICT = /^Verdict:\s*sound$/;
|
|
99
|
+
|
|
100
|
+
export const hasSoundVerdict = (advice: string) =>
|
|
101
|
+
SOUND_VERDICT.test(
|
|
102
|
+
(advice.split("\n").find((line) => line.trim()) ?? "").trim()
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
/** The single Advisor response header shared by tool and manual renderers. */
|
|
106
|
+
export const renderAdvisorResponseHeader = (sound: boolean, theme: Theme) =>
|
|
107
|
+
sound
|
|
108
|
+
? theme.fg("accent", theme.bold("◆ ADVISOR · SOUND"))
|
|
109
|
+
: theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE"));
|
|
110
|
+
|
|
111
|
+
export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
112
|
+
if (!advisorCollapseResponsesRef || expanded) {
|
|
113
|
+
return advice;
|
|
114
|
+
}
|
|
115
|
+
const lines = advice.split("\n");
|
|
116
|
+
if (lines.length <= COLLAPSED_ADVICE_LINES) {
|
|
117
|
+
return advice;
|
|
118
|
+
}
|
|
119
|
+
return `${lines.slice(0, COLLAPSED_ADVICE_LINES).join("\n")}\n\n… (${lines.length - COLLAPSED_ADVICE_LINES} more lines, Ctrl+O to expand)`;
|
|
120
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type Box, Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import { executorRef, getAdvisorSettings } from "../config/state.js";
|
|
8
|
+
import type { ScoutLifecycleEvent } from "../scout.js";
|
|
9
|
+
import { formatAdvisorUsage, snapshotAdvisorUsage } from "../usage.js";
|
|
10
|
+
import { renderThinkingMarkdown, SPINNER_FRAMES } from "./render-common.js";
|
|
11
|
+
import type { ScoutToolDetails } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export const scoutDetailsFromEvent = (
|
|
14
|
+
event: ScoutLifecycleEvent,
|
|
15
|
+
previous?: ScoutToolDetails
|
|
16
|
+
): ScoutToolDetails => {
|
|
17
|
+
if (event.type === "call") {
|
|
18
|
+
return { model: event.model, status: "calling" };
|
|
19
|
+
}
|
|
20
|
+
if (event.type === "chunk") {
|
|
21
|
+
return {
|
|
22
|
+
...previous,
|
|
23
|
+
model: event.model,
|
|
24
|
+
status: "streaming",
|
|
25
|
+
text: event.text,
|
|
26
|
+
thinking: event.thinking,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (event.type === "cancelled") {
|
|
30
|
+
return {
|
|
31
|
+
model: previous ? previous.model : executorRef,
|
|
32
|
+
status: "cancelled",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const { outcome } = event;
|
|
36
|
+
return outcome.ok
|
|
37
|
+
? {
|
|
38
|
+
availableCount: outcome.metrics.availableCount,
|
|
39
|
+
latencyMs: outcome.metrics.latencyMs,
|
|
40
|
+
model: outcome.model,
|
|
41
|
+
omittedBeforeScout: outcome.metrics.omittedBeforeScout,
|
|
42
|
+
selectedCount: outcome.metrics.selectedCount,
|
|
43
|
+
selectedLabels: outcome.selectedLabels,
|
|
44
|
+
status: "curated",
|
|
45
|
+
synthesis: outcome.selection.synthesis,
|
|
46
|
+
usage: snapshotAdvisorUsage(outcome.metrics.usage),
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
availableCount: outcome.metrics.availableCount,
|
|
50
|
+
fallbackReason: `${outcome.category}: ${outcome.message}`,
|
|
51
|
+
latencyMs: outcome.metrics.latencyMs,
|
|
52
|
+
model: outcome.model,
|
|
53
|
+
omittedBeforeScout: outcome.metrics.omittedBeforeScout,
|
|
54
|
+
selectedCount: 0,
|
|
55
|
+
status: "fallback",
|
|
56
|
+
usage: snapshotAdvisorUsage(outcome.metrics.usage),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const appendScoutLifecycleEntry = (
|
|
61
|
+
pi: ExtensionAPI,
|
|
62
|
+
event: ScoutLifecycleEvent,
|
|
63
|
+
previous?: ScoutToolDetails
|
|
64
|
+
) => {
|
|
65
|
+
const scout = scoutDetailsFromEvent(event, previous);
|
|
66
|
+
if (
|
|
67
|
+
event.type === "success" ||
|
|
68
|
+
event.type === "fallback" ||
|
|
69
|
+
event.type === "cancelled"
|
|
70
|
+
) {
|
|
71
|
+
pi.appendEntry?.("advisor-scout-result", scout);
|
|
72
|
+
}
|
|
73
|
+
return scout;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export class ScoutStatusManager {
|
|
77
|
+
readonly #active = new Set<symbol>();
|
|
78
|
+
readonly #known = new Set<symbol>();
|
|
79
|
+
readonly #retired = new Set<symbol>();
|
|
80
|
+
private readonly showStatus: boolean;
|
|
81
|
+
|
|
82
|
+
constructor(showStatus = true) {
|
|
83
|
+
this.showStatus = showStatus;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
register(token: symbol) {
|
|
87
|
+
if (!this.#retired.has(token)) {
|
|
88
|
+
this.#known.add(token);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
update(ctx: ExtensionContext, token: symbol, event: ScoutLifecycleEvent) {
|
|
93
|
+
if (this.#retired.has(token) || !ctx.hasUI) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.#known.add(token);
|
|
97
|
+
if (event.type === "call" || event.type === "chunk") {
|
|
98
|
+
this.#active.add(token);
|
|
99
|
+
if (this.showStatus) {
|
|
100
|
+
ctx.ui.setStatus("advisor-scout", "Scout curating…");
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.release(ctx, token);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
release(ctx: ExtensionContext, token: symbol) {
|
|
108
|
+
this.#active.delete(token);
|
|
109
|
+
this.#known.delete(token);
|
|
110
|
+
this.#retired.add(token);
|
|
111
|
+
if (!(ctx.hasUI && this.showStatus)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
ctx.ui.setStatus(
|
|
115
|
+
"advisor-scout",
|
|
116
|
+
this.#active.size > 0 ? "Scout curating…" : undefined
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clear(ctx: ExtensionContext) {
|
|
121
|
+
for (const token of this.#known) {
|
|
122
|
+
this.#retired.add(token);
|
|
123
|
+
}
|
|
124
|
+
this.#known.clear();
|
|
125
|
+
this.#active.clear();
|
|
126
|
+
if (ctx.hasUI && this.showStatus) {
|
|
127
|
+
ctx.ui.setStatus("advisor-scout", undefined);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const scoutTitle = (scout: ScoutToolDetails, frame: string) => {
|
|
133
|
+
if (scout.status === "calling" || scout.status === "streaming") {
|
|
134
|
+
return `◆ SCOUT ${frame} · CURATING…`;
|
|
135
|
+
}
|
|
136
|
+
if (scout.status === "curated") {
|
|
137
|
+
return "◆ SCOUT · CURATED";
|
|
138
|
+
}
|
|
139
|
+
if (scout.status === "cancelled") {
|
|
140
|
+
return "◆ SCOUT · CANCELLED";
|
|
141
|
+
}
|
|
142
|
+
return "◆ SCOUT · FALLBACK";
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const renderScoutDetails = (
|
|
146
|
+
box: Box,
|
|
147
|
+
scout: ScoutToolDetails,
|
|
148
|
+
expanded: boolean,
|
|
149
|
+
theme: Theme
|
|
150
|
+
) => {
|
|
151
|
+
const active = scout.status === "calling" || scout.status === "streaming";
|
|
152
|
+
const frame =
|
|
153
|
+
SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
|
|
154
|
+
const title = scoutTitle(scout, frame);
|
|
155
|
+
const lines = [
|
|
156
|
+
theme.fg(
|
|
157
|
+
scout.status === "fallback" || scout.status === "cancelled"
|
|
158
|
+
? "warning"
|
|
159
|
+
: "accent",
|
|
160
|
+
theme.bold(title)
|
|
161
|
+
),
|
|
162
|
+
theme.fg(
|
|
163
|
+
"dim",
|
|
164
|
+
` ${scout.model}${scout.selectedCount === undefined ? "" : ` · ${scout.selectedCount} kept / ${Math.max(0, (scout.availableCount ?? 0) - scout.selectedCount)} omitted`}${scout.latencyMs === undefined ? "" : ` · ${(scout.latencyMs / 1000).toFixed(1)}s`}`
|
|
165
|
+
),
|
|
166
|
+
];
|
|
167
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
168
|
+
const usage = formatAdvisorUsage(scout.usage);
|
|
169
|
+
if (usage) {
|
|
170
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (scout.fallbackReason) {
|
|
174
|
+
lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
|
|
175
|
+
}
|
|
176
|
+
const thinking = scout.thinking && active ? scout.thinking.slice(-200) : "";
|
|
177
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
178
|
+
if (thinking.trim()) {
|
|
179
|
+
box.addChild(renderThinkingMarkdown(thinking, theme));
|
|
180
|
+
}
|
|
181
|
+
const expandedLines: string[] = [];
|
|
182
|
+
if (expanded && scout.selectedLabels?.length) {
|
|
183
|
+
expandedLines.push(
|
|
184
|
+
theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (expanded && scout.synthesis) {
|
|
188
|
+
expandedLines.push(
|
|
189
|
+
theme.fg(
|
|
190
|
+
"dim",
|
|
191
|
+
` Scout synthesis (untrusted inference): ${scout.synthesis}`
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (expanded && scout.omittedBeforeScout) {
|
|
196
|
+
expandedLines.push(
|
|
197
|
+
theme.fg(
|
|
198
|
+
"dim",
|
|
199
|
+
` ${scout.omittedBeforeScout} group(s) omitted before Scout`
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (expandedLines.length > 0) {
|
|
204
|
+
box.addChild(new Text(expandedLines.join("\n"), 0, 0));
|
|
205
|
+
}
|
|
206
|
+
};
|