pi-advisor-flow 0.7.0 → 0.8.1-dev.1
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 +27 -18
- package/README.md +9 -8
- package/dist/index.js +1968 -1081
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/child-session.ts +15 -0
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +12 -3
- package/src/commands/lifecycle.ts +7 -1
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +102 -92
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +16 -2
- package/src/commands/types.ts +1 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/schema.ts +42 -17
- package/src/config/state.ts +12 -3
- package/src/config/storage.ts +34 -23
- package/src/config/types.ts +16 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +75 -85
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +26 -11
- package/src/jev/ledger.ts +58 -64
- package/src/jev/questions.ts +26 -23
- package/src/jev/state.ts +15 -12
- package/src/jev/transport.ts +8 -3
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +15 -11
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +128 -77
- package/src/session-state.ts +64 -49
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +43 -41
- package/src/tools/jev-turn-gate.ts +29 -9
- package/src/tools/loop-gate.ts +31 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +36 -16
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +146 -128
- package/src/tools/registration.ts +12 -14
- package/src/tools/render-advisor-result.ts +35 -22
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +1 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +65 -59
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +5 -11
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +38 -1
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +157 -70
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +14 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +26 -19
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import { appendOutcome } from "../outcomes.ts";
|
|
3
4
|
import type { AdvisorSessionState } from "../session-state.ts";
|
|
4
5
|
import { consultAdvisor, runAdvisorGate } from "./consultation.ts";
|
|
5
6
|
import { screenConsultation } from "./jev-filter.ts";
|
|
6
7
|
import { registerJevTurnGate } from "./jev-turn-gate.ts";
|
|
8
|
+
import type { JevTurnGateRegistration } from "./jev-turn-gate.ts";
|
|
7
9
|
import { registerAskAdvisorTool } from "./register-ask-advisor.ts";
|
|
8
10
|
import { registerToolLifecycle } from "./register-lifecycle.ts";
|
|
9
11
|
import { registerOutcomeTool } from "./register-outcome.ts";
|
|
@@ -35,18 +37,14 @@ export const registerAdvisorTool = (
|
|
|
35
37
|
registerToolLifecycle(registration);
|
|
36
38
|
registerAskAdvisorTool(registration);
|
|
37
39
|
registerOutcomeTool(registration);
|
|
38
|
-
|
|
39
|
-
(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
pi.sendMessage(message as never, { deliverAs: "steer" } as never),
|
|
49
|
-
session,
|
|
50
|
-
}
|
|
51
|
-
);
|
|
40
|
+
const turnGate: JevTurnGateRegistration = {
|
|
41
|
+
activeTools: () => pi.getActiveTools(),
|
|
42
|
+
consult: registration.consult,
|
|
43
|
+
send: (message) => pi.sendMessage(message, { deliverAs: "steer" }),
|
|
44
|
+
session,
|
|
45
|
+
};
|
|
46
|
+
if (dependencies.turnGateDeps) {
|
|
47
|
+
turnGate.deps = dependencies.turnGateDeps;
|
|
48
|
+
}
|
|
49
|
+
registerJevTurnGate((event, handler) => pi.on(event, handler), turnGate);
|
|
52
50
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
AgentToolResult,
|
|
4
|
+
Theme,
|
|
5
|
+
ToolRenderResultOptions,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
8
9
|
import { getAdvisorSettings } from "../config/state.ts";
|
|
9
10
|
import { textFrom } from "../conversation.ts";
|
|
10
11
|
import { formatAdvisorUsage } from "../usage.ts";
|
|
@@ -116,6 +117,33 @@ const renderPartialAdvisorResult = (
|
|
|
116
117
|
}
|
|
117
118
|
};
|
|
118
119
|
|
|
120
|
+
const thinkingPreview = (details: AdvisorToolDetails | undefined) =>
|
|
121
|
+
details?.thinking?.trim()
|
|
122
|
+
? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}`
|
|
123
|
+
: "";
|
|
124
|
+
|
|
125
|
+
const finalResultLines = (
|
|
126
|
+
details: AdvisorToolDetails | undefined,
|
|
127
|
+
advice: string,
|
|
128
|
+
theme: Theme
|
|
129
|
+
): string[] => {
|
|
130
|
+
const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
|
|
131
|
+
if (details?.advisor) {
|
|
132
|
+
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
133
|
+
}
|
|
134
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
135
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
136
|
+
if (usage) {
|
|
137
|
+
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const attachments = attachmentLabels(details);
|
|
141
|
+
if (attachments.length) {
|
|
142
|
+
lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
|
|
143
|
+
}
|
|
144
|
+
return lines;
|
|
145
|
+
};
|
|
146
|
+
|
|
119
147
|
const renderFinalAdvisorResult = (
|
|
120
148
|
box: Box,
|
|
121
149
|
result: AgentToolResult<AdvisorToolDetails>,
|
|
@@ -144,23 +172,8 @@ const renderFinalAdvisorResult = (
|
|
|
144
172
|
return;
|
|
145
173
|
}
|
|
146
174
|
const advice = details?.text || textFrom(result.content);
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
150
|
-
}
|
|
151
|
-
if (getAdvisorSettings().showUsageDetails) {
|
|
152
|
-
const usage = formatAdvisorUsage(details?.usage);
|
|
153
|
-
if (usage) {
|
|
154
|
-
lines.push(theme.fg("dim", ` Usage: ${usage}`));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const attachments = attachmentLabels(details);
|
|
158
|
-
if (attachments.length) {
|
|
159
|
-
lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
|
|
160
|
-
}
|
|
161
|
-
const thinking = details?.thinking?.trim()
|
|
162
|
-
? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}`
|
|
163
|
-
: "";
|
|
175
|
+
const thinking = thinkingPreview(details);
|
|
176
|
+
const lines = finalResultLines(details, advice, theme);
|
|
164
177
|
const displayAdvice = advice || "(Advisor returned no advice.)";
|
|
165
178
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
166
179
|
if (thinking) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { getMarkdownTheme
|
|
1
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
|
-
type Component,
|
|
5
5
|
Markdown,
|
|
6
6
|
Text,
|
|
7
7
|
truncateToWidth,
|
|
8
8
|
visibleWidth,
|
|
9
9
|
} from "@earendil-works/pi-tui";
|
|
10
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
10
12
|
import { advisorCollapseResponsesRef } from "../config/state.ts";
|
|
11
13
|
import { piHideThinkingEnabled } from "../pi-settings.ts";
|
|
12
14
|
|
|
@@ -27,6 +29,9 @@ const THINKING_PREFIX = " ";
|
|
|
27
29
|
const THINKING_PREFIX_WIDTH = visibleWidth(THINKING_PREFIX);
|
|
28
30
|
export type ThinkingTheme = Pick<Theme, "fg">;
|
|
29
31
|
|
|
32
|
+
/** Shared no-op for interface-required methods that manage no state. */
|
|
33
|
+
export const noop = (): void => undefined;
|
|
34
|
+
|
|
30
35
|
/** Renders nested-model thinking as Markdown with the compact speech-bubble cue; the prefix is added after parsing so it cannot change block syntax. */
|
|
31
36
|
class ThinkingMarkdown implements Component {
|
|
32
37
|
private readonly markdown: Markdown;
|
|
@@ -59,28 +64,20 @@ class ThinkingMarkdown implements Component {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
/** Collapsed placeholder shown when Pi's hide_thinking setting is on. */
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
render(): string[] {
|
|
70
|
-
return [this.label];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
invalidate(): void {
|
|
74
|
-
// The collapsed label is static.
|
|
75
|
-
}
|
|
76
|
-
}
|
|
67
|
+
const hiddenThinkingLabel = (theme: ThinkingTheme): Component => {
|
|
68
|
+
const label = theme.fg("thinkingText", `${THINKING_PREFIX}Thinking…`);
|
|
69
|
+
return {
|
|
70
|
+
invalidate: noop,
|
|
71
|
+
render: () => [label],
|
|
72
|
+
};
|
|
73
|
+
};
|
|
77
74
|
|
|
78
75
|
export const renderThinkingMarkdown = (
|
|
79
76
|
thinking: string,
|
|
80
77
|
theme: ThinkingTheme
|
|
81
78
|
): Component =>
|
|
82
79
|
piHideThinkingEnabled()
|
|
83
|
-
?
|
|
80
|
+
? hiddenThinkingLabel(theme)
|
|
84
81
|
: new ThinkingMarkdown(thinking, theme);
|
|
85
82
|
|
|
86
83
|
export const resolveAdvisorRequest = (question?: string) =>
|
|
@@ -107,7 +104,7 @@ export const renderAdvisorCallBox = (
|
|
|
107
104
|
|
|
108
105
|
const COLLAPSED_ADVICE_LINES = 12;
|
|
109
106
|
// The system prompt requires this exact first line, so the match is exact too.
|
|
110
|
-
const SOUND_VERDICT = /^Verdict:\s*sound
|
|
107
|
+
const SOUND_VERDICT = /^Verdict:\s*sound$/u;
|
|
111
108
|
|
|
112
109
|
export const hasSoundVerdict = (advice: string) =>
|
|
113
110
|
SOUND_VERDICT.test(
|
|
@@ -3,7 +3,9 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
Theme,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { Box } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
7
9
|
import { executorRef, getAdvisorSettings } from "../config/state.ts";
|
|
8
10
|
import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
9
11
|
import { formatAdvisorUsage, snapshotAdvisorUsage } from "../usage.ts";
|
|
@@ -142,6 +144,43 @@ const scoutTitle = (scout: ScoutToolDetails, frame: string) => {
|
|
|
142
144
|
return "◆ SCOUT · FALLBACK";
|
|
143
145
|
};
|
|
144
146
|
|
|
147
|
+
const scoutSummaryLine = (scout: ScoutToolDetails) =>
|
|
148
|
+
` ${scout.model}${
|
|
149
|
+
scout.selectedCount === undefined
|
|
150
|
+
? ""
|
|
151
|
+
: ` · ${scout.selectedCount} kept / ${Math.max(0, (scout.availableCount ?? 0) - scout.selectedCount)} omitted`
|
|
152
|
+
}${scout.latencyMs === undefined ? "" : ` · ${(scout.latencyMs / 1000).toFixed(1)}s`}`;
|
|
153
|
+
|
|
154
|
+
const scoutExpandedLines = (
|
|
155
|
+
scout: ScoutToolDetails,
|
|
156
|
+
expanded: boolean,
|
|
157
|
+
theme: Theme
|
|
158
|
+
): string[] => {
|
|
159
|
+
const lines: string[] = [];
|
|
160
|
+
if (expanded && scout.selectedLabels?.length) {
|
|
161
|
+
lines.push(
|
|
162
|
+
theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (expanded && scout.synthesis) {
|
|
166
|
+
lines.push(
|
|
167
|
+
theme.fg(
|
|
168
|
+
"dim",
|
|
169
|
+
` Scout synthesis (untrusted inference): ${scout.synthesis}`
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (expanded && scout.omittedBeforeScout) {
|
|
174
|
+
lines.push(
|
|
175
|
+
theme.fg(
|
|
176
|
+
"dim",
|
|
177
|
+
` ${scout.omittedBeforeScout} group(s) omitted before Scout`
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return lines;
|
|
182
|
+
};
|
|
183
|
+
|
|
145
184
|
export const renderScoutDetails = (
|
|
146
185
|
box: Box,
|
|
147
186
|
scout: ScoutToolDetails,
|
|
@@ -159,10 +198,7 @@ export const renderScoutDetails = (
|
|
|
159
198
|
: "accent",
|
|
160
199
|
theme.bold(title)
|
|
161
200
|
),
|
|
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
|
-
),
|
|
201
|
+
theme.fg("dim", scoutSummaryLine(scout)),
|
|
166
202
|
];
|
|
167
203
|
if (getAdvisorSettings().showUsageDetails) {
|
|
168
204
|
const usage = formatAdvisorUsage(scout.usage);
|
|
@@ -178,28 +214,7 @@ export const renderScoutDetails = (
|
|
|
178
214
|
if (thinking.trim()) {
|
|
179
215
|
box.addChild(renderThinkingMarkdown(thinking, theme));
|
|
180
216
|
}
|
|
181
|
-
const expandedLines
|
|
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
|
-
}
|
|
217
|
+
const expandedLines = scoutExpandedLines(scout, expanded, theme);
|
|
203
218
|
if (expandedLines.length > 0) {
|
|
204
219
|
box.addChild(new Text(expandedLines.join("\n"), 0, 0));
|
|
205
220
|
}
|
package/src/tools/types.ts
CHANGED
package/src/tools.ts
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type Focusable,
|
|
5
|
-
Key,
|
|
6
|
-
matchesKey,
|
|
7
|
-
truncateToWidth,
|
|
8
|
-
} from "@earendil-works/pi-tui";
|
|
2
|
+
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { Component, Focusable } from "@earendil-works/pi-tui";
|
|
9
4
|
import { noul } from "@typesafe-ai/sdk";
|
|
5
|
+
|
|
10
6
|
import { jevClientFromCredentials } from "../jev/client.ts";
|
|
11
7
|
import {
|
|
12
8
|
clearKeyTypeSafeKey,
|
|
13
9
|
consumePlaintextKeyWarning,
|
|
14
|
-
type JevKeyStoreResult,
|
|
15
10
|
removeTypeSafeKeyFromAdvisorJson,
|
|
16
11
|
writeKeyTypeSafeKey,
|
|
17
12
|
} from "../jev/key-store.ts";
|
|
18
|
-
import
|
|
13
|
+
import type { JevKeyStoreResult } from "../jev/key-store.ts";
|
|
14
|
+
import { resolveJevTransport } from "../jev/transport.ts";
|
|
15
|
+
import type { JevCredentials } from "../jev/transport.ts";
|
|
19
16
|
import { MaskedInput } from "./masked-input.ts";
|
|
20
17
|
import type { RenderRequester } from "./types.ts";
|
|
21
18
|
|
|
@@ -48,19 +45,40 @@ type SetupAction =
|
|
|
48
45
|
| "enter-key"
|
|
49
46
|
| "done";
|
|
50
47
|
|
|
48
|
+
const ACTION_LABELS: Record<SetupAction, string> = {
|
|
49
|
+
disable: "Disable",
|
|
50
|
+
"disable-clear": "Disable and clear stored key",
|
|
51
|
+
done: "Done",
|
|
52
|
+
"enter-key": "Enter a TypeSafe API key",
|
|
53
|
+
"verify-again": "Verify again",
|
|
54
|
+
"verify-enable": "Verify and enable",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const fireAndForget = async (action: Promise<unknown>): Promise<void> => {
|
|
58
|
+
try {
|
|
59
|
+
await action;
|
|
60
|
+
} catch {
|
|
61
|
+
// Background setup actions surface their own notices.
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
51
65
|
const transportLabel = (credentials: JevCredentials): string => {
|
|
52
66
|
if (credentials.transport === "openrouter") {
|
|
53
67
|
return "OpenRouter (reusing pi login)";
|
|
54
68
|
}
|
|
55
69
|
switch (credentials.source) {
|
|
56
|
-
case "advisor-json":
|
|
70
|
+
case "advisor-json": {
|
|
57
71
|
return "TypeSafe (key: advisor.json — plaintext, not recommended)";
|
|
58
|
-
|
|
72
|
+
}
|
|
73
|
+
case "bun-secrets": {
|
|
59
74
|
return "TypeSafe (key: Bun.secrets)";
|
|
60
|
-
|
|
75
|
+
}
|
|
76
|
+
case "file": {
|
|
61
77
|
return "TypeSafe (key: stored file, mode 0600)";
|
|
62
|
-
|
|
78
|
+
}
|
|
79
|
+
default: {
|
|
63
80
|
return "TypeSafe (key: TYPESAFE_API_KEY)";
|
|
81
|
+
}
|
|
64
82
|
}
|
|
65
83
|
};
|
|
66
84
|
|
|
@@ -107,7 +125,7 @@ export class JevSetupSubmenu implements Component, Focusable {
|
|
|
107
125
|
onSubmit: (value) => this.submitEnteredKey(value),
|
|
108
126
|
placeholder: "Paste a TypeSafe API key",
|
|
109
127
|
});
|
|
110
|
-
this.refresh()
|
|
128
|
+
void fireAndForget(this.refresh());
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
get focused(): boolean {
|
|
@@ -135,14 +153,14 @@ export class JevSetupSubmenu implements Component, Focusable {
|
|
|
135
153
|
const actions = this.actions();
|
|
136
154
|
if (
|
|
137
155
|
matchesKey(keyData, Key.down) ||
|
|
138
|
-
keyData === "\
|
|
139
|
-
keyData === "\
|
|
156
|
+
keyData === "\u001B[B" ||
|
|
157
|
+
keyData === "\u001BOB"
|
|
140
158
|
) {
|
|
141
159
|
this.selectedIndex = (this.selectedIndex + 1) % actions.length;
|
|
142
160
|
} else if (
|
|
143
161
|
matchesKey(keyData, Key.up) ||
|
|
144
|
-
keyData === "\
|
|
145
|
-
keyData === "\
|
|
162
|
+
keyData === "\u001B[A" ||
|
|
163
|
+
keyData === "\u001BOA"
|
|
146
164
|
) {
|
|
147
165
|
this.selectedIndex =
|
|
148
166
|
(this.selectedIndex - 1 + actions.length) % actions.length;
|
|
@@ -162,28 +180,29 @@ export class JevSetupSubmenu implements Component, Focusable {
|
|
|
162
180
|
"",
|
|
163
181
|
];
|
|
164
182
|
if (this.credentials) {
|
|
165
|
-
lines.push(
|
|
166
|
-
|
|
183
|
+
lines.push(
|
|
184
|
+
` Transport: ${transportLabel(this.credentials)}`,
|
|
185
|
+
` Filter: ${enabled ? "On" : "Off"}`
|
|
186
|
+
);
|
|
167
187
|
} else if (this.mode === "verifying") {
|
|
168
188
|
lines.push(" Checking available Jev credentials…");
|
|
169
189
|
} else {
|
|
170
190
|
lines.push(
|
|
171
|
-
" No Jev credentials found. Enter a TypeSafe API key below, or add"
|
|
191
|
+
" No Jev credentials found. Enter a TypeSafe API key below, or add",
|
|
192
|
+
" an OpenRouter login in pi; it is reused automatically."
|
|
172
193
|
);
|
|
173
|
-
lines.push(" an OpenRouter login in pi; it is reused automatically.");
|
|
174
194
|
}
|
|
175
195
|
if (this.notice) {
|
|
176
|
-
lines.push("");
|
|
177
|
-
lines.push(theme.fg("warning", ` ${this.notice}`));
|
|
196
|
+
lines.push("", theme.fg("warning", ` ${this.notice}`));
|
|
178
197
|
}
|
|
179
198
|
lines.push("");
|
|
180
199
|
if (this.mode === "verifying") {
|
|
181
200
|
lines.push(" Verifying with a live Jev call…");
|
|
182
201
|
} else if (this.mode === "key-entry") {
|
|
183
202
|
lines.push(
|
|
184
|
-
` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}
|
|
203
|
+
` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`,
|
|
204
|
+
theme.fg("dim", " Enter: verify · Esc: cancel")
|
|
185
205
|
);
|
|
186
|
-
lines.push(theme.fg("dim", " Enter: verify · Esc: cancel"));
|
|
187
206
|
} else {
|
|
188
207
|
for (const [index, label] of this.labels().entries()) {
|
|
189
208
|
const prefix = index === this.selectedIndex ? "→ " : " ";
|
|
@@ -210,20 +229,8 @@ export class JevSetupSubmenu implements Component, Focusable {
|
|
|
210
229
|
return actions;
|
|
211
230
|
}
|
|
212
231
|
|
|
213
|
-
private actionLabels(): Record<SetupAction, string> {
|
|
214
|
-
return {
|
|
215
|
-
disable: "Disable",
|
|
216
|
-
"disable-clear": "Disable and clear stored key",
|
|
217
|
-
done: "Done",
|
|
218
|
-
"enter-key": "Enter a TypeSafe API key",
|
|
219
|
-
"verify-again": "Verify again",
|
|
220
|
-
"verify-enable": "Verify and enable",
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
232
|
private labels(): string[] {
|
|
225
|
-
|
|
226
|
-
return this.actions().map((action) => labels[action]);
|
|
233
|
+
return this.actions().map((action) => ACTION_LABELS[action]);
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
private async refresh(): Promise<void> {
|
|
@@ -242,26 +249,25 @@ export class JevSetupSubmenu implements Component, Focusable {
|
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
private activate(action: SetupAction): void {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return;
|
|
252
|
+
if (action === "done") {
|
|
253
|
+
this.options.done();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (action === "enter-key") {
|
|
257
|
+
this.mode = "key-entry";
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (action === "disable") {
|
|
261
|
+
this.notice = undefined;
|
|
262
|
+
this.options.done("Off");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (action === "disable-clear") {
|
|
266
|
+
void fireAndForget(this.disableAndClear());
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (action === "verify-again" || action === "verify-enable") {
|
|
270
|
+
void fireAndForget(this.verifyAndEnable());
|
|
265
271
|
}
|
|
266
272
|
}
|
|
267
273
|
|
package/src/ui/manual-dialog.ts
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type KeyId,
|
|
9
|
-
matchesKey,
|
|
1
|
+
import { Editor, Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
import type {
|
|
3
|
+
Component,
|
|
4
|
+
EditorTheme,
|
|
5
|
+
Focusable,
|
|
6
|
+
Keybindings,
|
|
7
|
+
KeyId,
|
|
10
8
|
} from "@earendil-works/pi-tui";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type GitContextLevel,
|
|
15
|
-
} from "../git.ts";
|
|
9
|
+
|
|
10
|
+
import { clampGitContextLevel, GIT_CONTEXT_LEVELS } from "../git.ts";
|
|
11
|
+
import type { GitContextLevel } from "../git.ts";
|
|
16
12
|
import { renderManualAdvisorDialog } from "./manual-dialog-render.ts";
|
|
17
13
|
import type {
|
|
18
14
|
ManualAdvisorDialogOptions,
|
|
19
15
|
ManualAdvisorFocus,
|
|
16
|
+
ManualAdvisorRequest,
|
|
20
17
|
} from "./types.ts";
|
|
21
18
|
|
|
22
|
-
//
|
|
19
|
+
// SAFETY: the joined literal is exactly the registered "tui.input.tab" keybinding path.
|
|
23
20
|
const TUI_INPUT_TAB: keyof Keybindings = ["tui", "input", "tab"].join(
|
|
24
21
|
"."
|
|
25
22
|
) as keyof Keybindings;
|
|
26
23
|
|
|
24
|
+
interface ManualDialogState {
|
|
25
|
+
completed: boolean;
|
|
26
|
+
focused: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isShiftTab = (keyData: string): boolean =>
|
|
30
|
+
matchesKey(keyData, Key.shift("tab"));
|
|
31
|
+
|
|
27
32
|
/** The small, non-persistent /advisor-manual form; callers own budget and consultation decisions. */
|
|
28
33
|
export class ManualAdvisorDialog implements Component, Focusable {
|
|
29
34
|
private readonly options: ManualAdvisorDialogOptions;
|
|
@@ -32,7 +37,7 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
32
37
|
private gitIndex: number;
|
|
33
38
|
private actionIndex = 0;
|
|
34
39
|
private focusTarget: ManualAdvisorFocus = "editor";
|
|
35
|
-
private readonly state:
|
|
40
|
+
private readonly state: ManualDialogState = {
|
|
36
41
|
completed: false,
|
|
37
42
|
focused: false,
|
|
38
43
|
};
|
|
@@ -54,7 +59,7 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
54
59
|
// while the configured level remains the hard ceiling.
|
|
55
60
|
this.gitLevels = GIT_CONTEXT_LEVELS.filter(
|
|
56
61
|
(level) => clampGitContextLevel(level, options.gitContext) === level
|
|
57
|
-
).
|
|
62
|
+
).toReversed();
|
|
58
63
|
this.gitIndex = Math.max(0, this.gitLevels.indexOf(options.gitContext));
|
|
59
64
|
|
|
60
65
|
const editorTheme: EditorTheme = {
|
|
@@ -90,7 +95,7 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
90
95
|
this.cancel();
|
|
91
96
|
return;
|
|
92
97
|
}
|
|
93
|
-
if (
|
|
98
|
+
if (isShiftTab(keyData)) {
|
|
94
99
|
this.changeFocus(-1);
|
|
95
100
|
return;
|
|
96
101
|
}
|
|
@@ -98,24 +103,12 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
98
103
|
this.changeFocus(1);
|
|
99
104
|
return;
|
|
100
105
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// form control so the Git selector is reachable without Tab.
|
|
108
|
-
this.handleEditorInput(keyData);
|
|
109
|
-
return;
|
|
110
|
-
case "git":
|
|
111
|
-
this.handleGitInput(keyData);
|
|
112
|
-
return;
|
|
113
|
-
case "actions":
|
|
114
|
-
this.handleActionInput(keyData);
|
|
115
|
-
return;
|
|
116
|
-
default:
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
106
|
+
const handlers: Record<ManualAdvisorFocus, (keyData: string) => void> = {
|
|
107
|
+
actions: (key) => this.handleActionInput(key),
|
|
108
|
+
editor: (key) => this.handleEditorInput(key),
|
|
109
|
+
git: (key) => this.handleGitInput(key),
|
|
110
|
+
};
|
|
111
|
+
handlers[this.focusTarget](keyData);
|
|
119
112
|
}
|
|
120
113
|
|
|
121
114
|
render(width: number): string[] {
|
|
@@ -137,6 +130,10 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
137
130
|
}
|
|
138
131
|
|
|
139
132
|
private handleEditorInput(keyData: string): void {
|
|
133
|
+
// Editor owns Enter/Shift+Enter and all cursor/editing semantics. Its
|
|
134
|
+
// onSubmit callback above is the sole editor submission path. At the
|
|
135
|
+
// edge of the message, a directional key also moves to the adjacent
|
|
136
|
+
// form control so the Git selector is reachable without Tab.
|
|
140
137
|
let direction: -1 | 1 | undefined;
|
|
141
138
|
if (this.matches(keyData, "tui.editor.cursorDown", Key.down)) {
|
|
142
139
|
direction = 1;
|
|
@@ -257,10 +254,6 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
257
254
|
);
|
|
258
255
|
}
|
|
259
256
|
|
|
260
|
-
private isShiftTab(keyData: string): boolean {
|
|
261
|
-
return matchesKey(keyData, Key.shift("tab"));
|
|
262
|
-
}
|
|
263
|
-
|
|
264
257
|
private isCancel(keyData: string): boolean {
|
|
265
258
|
return (
|
|
266
259
|
matchesKey(keyData, Key.escape) ||
|
|
@@ -284,10 +277,13 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
284
277
|
return;
|
|
285
278
|
}
|
|
286
279
|
this.state.completed = true;
|
|
287
|
-
|
|
280
|
+
const request: ManualAdvisorRequest = {
|
|
288
281
|
gitContext: this.gitLevels[this.gitIndex] ?? "off",
|
|
289
|
-
|
|
290
|
-
|
|
282
|
+
};
|
|
283
|
+
if (message) {
|
|
284
|
+
request.message = message;
|
|
285
|
+
}
|
|
286
|
+
this.options.onSubmit(request);
|
|
291
287
|
}
|
|
292
288
|
|
|
293
289
|
private cancel(): void {
|