pi-advisor-flow 0.5.1 → 0.5.3
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 +21 -0
- package/README.md +3 -2
- package/extensions/index.ts +7 -7
- package/package.json +2 -1
- 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 -1259
- 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 -814
- package/src/conversation.ts +4 -2
- package/src/herdr.ts +1 -1
- package/src/model-stream.ts +15 -1
- 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 +247 -0
- package/src/tools/register-lifecycle.ts +119 -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 -1865
- 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,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
advisorSessionSummaryRef,
|
|
3
|
+
getAdvisorMaxCallsPerSession,
|
|
4
|
+
isSimpleMode,
|
|
5
|
+
} from "../config/state.js";
|
|
6
|
+
import { loadConfig } from "../config/storage.js";
|
|
7
|
+
import { herdrAdvisorBlock } from "../herdr.js";
|
|
8
|
+
import { reserveAdvisorCall } from "./gate-policy.js";
|
|
9
|
+
import { handleAutomaticGate } from "./loop-gate.js";
|
|
10
|
+
import { advisorInvocationGuidelines } from "./prompts.js";
|
|
11
|
+
import type { ToolRegistrationContext } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export const registerToolLifecycle = ({
|
|
14
|
+
pi,
|
|
15
|
+
reservedCalls,
|
|
16
|
+
runGate,
|
|
17
|
+
scoutStatus,
|
|
18
|
+
session,
|
|
19
|
+
}: ToolRegistrationContext): void => {
|
|
20
|
+
// A throwing tool_call handler makes pi block that tool call (fail-safe), so
|
|
21
|
+
// a malformed advisor.json must not escape this handler: gating is skipped
|
|
22
|
+
// for that call and the failure is surfaced once per distinct outage. Config
|
|
23
|
+
// errors stay an Advisor concern instead of escalating into a session-wide
|
|
24
|
+
// outage.
|
|
25
|
+
let lastConfigErrorNotified: string | undefined;
|
|
26
|
+
const loadConfigOrSkipGating = (ctx: Parameters<typeof loadConfig>[0]) => {
|
|
27
|
+
try {
|
|
28
|
+
loadConfig(ctx);
|
|
29
|
+
lastConfigErrorNotified = undefined;
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
if (message !== lastConfigErrorNotified) {
|
|
34
|
+
lastConfigErrorNotified = message;
|
|
35
|
+
if (ctx.hasUI) {
|
|
36
|
+
ctx.ui.notify(
|
|
37
|
+
`Advisor gating skipped; configuration is invalid. ${message} Fix advisor.json.`,
|
|
38
|
+
"error"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
pi.on("session_start", (_event, ctx) => {
|
|
47
|
+
session.resetTask();
|
|
48
|
+
reservedCalls.clear();
|
|
49
|
+
herdrAdvisorBlock.clear();
|
|
50
|
+
if (ctx?.hasUI) {
|
|
51
|
+
ctx.ui.setStatus("advisor-usage", undefined);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
56
|
+
if (!pi.getActiveTools().includes("ask_advisor")) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
loadConfig(ctx);
|
|
60
|
+
const guidelines = advisorInvocationGuidelines();
|
|
61
|
+
const budget = isSimpleMode()
|
|
62
|
+
? undefined
|
|
63
|
+
: session.remainingCalls(getAdvisorMaxCallsPerSession());
|
|
64
|
+
if (budget !== undefined) {
|
|
65
|
+
guidelines.push(
|
|
66
|
+
`Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return guidelines.length > 0
|
|
70
|
+
? {
|
|
71
|
+
systemPrompt: `${ctx.getSystemPrompt()}\n\nAdvisor invocation settings:\n${guidelines.map((rule) => `- ${rule}`).join("\n")}`,
|
|
72
|
+
}
|
|
73
|
+
: undefined;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
pi.on("tool_call", (event, ctx) => {
|
|
77
|
+
if (session.blocked) {
|
|
78
|
+
return {
|
|
79
|
+
block: true,
|
|
80
|
+
reason: session.blockedReason ?? "Advisor session is blocked.",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (!pi.getActiveTools().includes("ask_advisor")) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!loadConfigOrSkipGating(ctx)) {
|
|
87
|
+
// The ask_advisor execute path surfaces its own configuration errors as
|
|
88
|
+
// tool errors; every other tool must proceed without Advisor gating.
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const reservation = reserveAdvisorCall(event, ctx, session, reservedCalls);
|
|
92
|
+
if (event.toolName === "ask_advisor") {
|
|
93
|
+
return reservation;
|
|
94
|
+
}
|
|
95
|
+
return handleAutomaticGate(pi, event, ctx, session, runGate, scoutStatus);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
99
|
+
// Any reservation still present never reached execute (for example because
|
|
100
|
+
// another handler blocked it or the turn was aborted).
|
|
101
|
+
reservedCalls.clear();
|
|
102
|
+
if (isSimpleMode() || session.blocked || !advisorSessionSummaryRef) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const summary = session.summary(getAdvisorMaxCallsPerSession());
|
|
106
|
+
if (summary && ctx.hasUI) {
|
|
107
|
+
ctx.ui.notify(summary, "info");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
112
|
+
reservedCalls.clear();
|
|
113
|
+
scoutStatus.clear(ctx);
|
|
114
|
+
herdrAdvisorBlock.clear();
|
|
115
|
+
if (ctx?.hasUI) {
|
|
116
|
+
ctx.ui.setStatus("advisor-usage", undefined);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
};
|
|
@@ -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
|
+
};
|