@diousk/pi-subagents-fast 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +13 -10
- package/dist/agent-manager.js +3 -1
- package/dist/index.js +1 -1
- package/dist/model-routing.d.ts +5 -1
- package/dist/model-routing.js +11 -8
- package/dist/routing-config.d.ts +3 -0
- package/dist/routing-config.js +6 -2
- package/dist/ui/model-routing-menu.d.ts +2 -1
- package/dist/ui/model-routing-menu.js +14 -5
- package/dist/ui/routing-status.d.ts +5 -0
- package/dist/ui/routing-status.js +113 -0
- package/docs/rpc.md +2 -2
- package/docs/workflows.md +1 -1
- package/package.json +1 -1
- package/src/agent-manager.ts +3 -1
- package/src/index.ts +1 -1
- package/src/model-routing.ts +15 -9
- package/src/routing-config.ts +9 -3
- package/src/ui/model-routing-menu.ts +14 -5
- package/src/ui/routing-status.ts +97 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.24.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Routing runtime status in `/agents`.** Open Model routing → Runtime status to inspect effective configuration, credential presence, eligible profiles and the latest retained routing decisions, including fallback reasons and actual model/thinking. The refreshable, read-only view hides API keys and makes no classifier request.
|
|
14
|
+
|
|
15
|
+
## [0.23.0] - 2026-10-02
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **BREAKING: Jev model profiles require `thinkingLevel`.** Add `minimal`, `low`, `medium`, `high`, `xhigh` or `max` to every `jev.models` entry. Successful routing applies the model and thinking level together; shadow and fallback preserve the original pair. The routing menu requires a level, and invalid or missing levels disable the Jev block with a diagnostic.
|
|
19
|
+
|
|
10
20
|
## [0.22.0] - 2026-10-02
|
|
11
21
|
|
|
12
22
|
> **Breaking: requires Pi 1.0.0 or newer.** Update Pi before using this version.
|
package/README.md
CHANGED
|
@@ -634,7 +634,9 @@ When background agents complete, they notify the main agent. The **join mode** c
|
|
|
634
634
|
|
|
635
635
|
## Model routing
|
|
636
636
|
|
|
637
|
-
Open `/agents → Model routing` to choose a mode and configure a guideline path, models with descriptions, or a masked TypeSafe API key. The menu shows the active mode and source and saves project settings. For machine-wide defaults, edit `~/.pi/agent/subagents.json`; project overrides go in `.pi/subagents.json`. `PI_CODING_AGENT_DIR` changes the global directory along with Pi's other configuration.
|
|
637
|
+
Open `/agents → Model routing` to choose a mode and configure a guideline path, models with descriptions and required thinking levels, or a masked TypeSafe API key. The menu shows the active mode and source and saves project settings. For machine-wide defaults, edit `~/.pi/agent/subagents.json`; project overrides go in `.pi/subagents.json`. `PI_CODING_AGENT_DIR` changes the global directory along with Pi's other configuration.
|
|
638
|
+
|
|
639
|
+
Use `/agents → Model routing → Runtime status` to inspect the effective mode and its project/global source, routing priority and fallback, guideline path, configured model/thinking profiles, current candidate eligibility, classifier availability and credential presence. No API key is displayed and opening the page does not classify a task. Credential presence does not confirm that the service will accept it. The page also shows the latest ten retained agent decisions, including selected versus actual model/thinking, shadow suggestions, confidence, fallback/error reasons and classifier usage. This is an in-memory view, not a durable log: evicted records and previous sessions are not included. Press `r` to refresh, Up/Down to scroll and Esc to return.
|
|
638
640
|
|
|
639
641
|
Set one top-level field to control routing; omitting it means `auto`:
|
|
640
642
|
|
|
@@ -660,7 +662,7 @@ The default priority is:
|
|
|
660
662
|
| 3 | `jev` is configured | Jev chooses a model from your descriptions for a fresh delegated task |
|
|
661
663
|
| 4 | None of the above | The existing model is used |
|
|
662
664
|
|
|
663
|
-
**Custom agents:** keep using `~/.pi/agent/agents/<name>.md`, `.pi/agents/<name>.md` or `.agents/agents/<name>.md`. No routing setting is needed. Built-in and disabled agents do not activate priority 1. Agent-file model/thinking pins supply the default choice over `Agent` parameters; a confident Jev choice in `jev` mode can replace
|
|
665
|
+
**Custom agents:** keep using `~/.pi/agent/agents/<name>.md`, `.pi/agents/<name>.md` or `.agents/agents/<name>.md`. No routing setting is needed. Built-in and disabled agents do not activate priority 1. Agent-file model/thinking pins supply the default choice over `Agent` parameters; a confident Jev choice in `jev` mode can replace both model and thinking level.
|
|
664
666
|
|
|
665
667
|
**Custom guideline:** write your routing rules in `~/.pi/agent/agents/custom-route.md`, then configure:
|
|
666
668
|
|
|
@@ -679,16 +681,16 @@ For example, the Markdown can say “Use anthropic/claude-haiku-4-5 for simple e
|
|
|
679
681
|
"jev": {
|
|
680
682
|
"TYPESAFE_API_KEY": "your-typesafe-key",
|
|
681
683
|
"models": [
|
|
682
|
-
{ "model": "anthropic/claude-haiku-4-5", "description": "Simple edits, lookup and concise summaries" },
|
|
683
|
-
{ "model": "openai-codex/gpt-6.1-sol", "description": "Complex debugging, architecture and concurrency" }
|
|
684
|
+
{ "model": "anthropic/claude-haiku-4-5", "description": "Simple edits, lookup and concise summaries", "thinkingLevel": "low" },
|
|
685
|
+
{ "model": "openai-codex/gpt-6.1-sol", "description": "Complex debugging, architecture and concurrency", "thinkingLevel": "high" }
|
|
684
686
|
]
|
|
685
687
|
}
|
|
686
688
|
}
|
|
687
689
|
```
|
|
688
690
|
|
|
689
|
-
`TYPESAFE_API_KEY` is optional when Pi already has TypeSafe credentials or the environment variable is set. A literal key must be a nonempty token without whitespace or control characters and applies only to that classifier request; it is saved in the settings file, masked in the menu and omitted from settings events, prompts and routing records. Without a literal key, Pi's native credential availability check runs before classification. Rejected credentials fall back without retrying. The extension does not change environment variables or register a routing provider. `models` accepts 1–254 unique entries with descriptions of 1–4000 characters. Jev entries use exact `provider/model-id` spelling; fuzzy names remain available for explicit `Agent` parameters.
|
|
691
|
+
`TYPESAFE_API_KEY` is optional when Pi already has TypeSafe credentials or the environment variable is set. A literal key must be a nonempty token without whitespace or control characters and applies only to that classifier request; it is saved in the settings file, masked in the menu and omitted from settings events, prompts and routing records. Without a literal key, Pi's native credential availability check runs before classification. Rejected credentials fall back without retrying. The extension does not change environment variables or register a routing provider. `models` accepts 1–254 unique entries with descriptions of 1–4000 characters. Every entry requires `thinkingLevel`: `minimal`, `low`, `medium`, `high`, `xhigh` or `max`. Existing profiles must add this field; a missing or invalid value disables the whole Jev block and uses the default-priority fallback. Jev entries use exact `provider/model-id` spelling; fuzzy names remain available for explicit `Agent` parameters.
|
|
690
692
|
|
|
691
|
-
Under `auto`, supplying **either** `model` or `thinking` explicitly skips Jev. An agent-file pin also skips it. In workflows, either `model` or `effort` skips it, and workflow options retain their precedence over agent-file defaults. Inherited models remain eligible for Jev. Under `jev`, these choices supply the fallback model, but do not skip classification. Under `shadow`, they remain the actual choice while Jev records a comparison. Jev
|
|
693
|
+
Under `auto`, supplying **either** `model` or `thinking` explicitly skips Jev. An agent-file pin also skips it. In workflows, either `model` or `effort` skips it, and workflow options retain their precedence over agent-file defaults. Inherited models remain eligible for Jev. Under `jev`, these choices supply the fallback model, but do not skip classification. Under `shadow`, they remain the actual choice while Jev records a comparison. A selected Jev profile applies both its model and `thinkingLevel`. Pi clamps the requested thinking level to what that model supports. Shadow and fallback preserve the original model and thinking level.
|
|
692
694
|
|
|
693
695
|
Automatic choices are limited to authenticated models and the current nonempty Pi model scope, regardless of the `scopeModels` setting. When `scopeModels` is enabled, Jev candidates also respect `enabledModels`. Unavailable candidates are excluded and the selected model is checked again after classification, including any changed scope. Jev failure, confidence below 0.6 or a two-second timeout keeps the default-priority model already selected by the main agent, explicit caller or agent definition, otherwise the existing model. This fallback does not make a second Jev request. Cancellation stops startup without launching a fallback. Queued agents read the current mode and classify after dequeue; nested calls share a separate classifier concurrency limit of four and do not take another agent slot.
|
|
694
696
|
|
|
@@ -696,7 +698,7 @@ Jev applies to fresh tool, nested, workflow, scheduled, RPC and direct-mention s
|
|
|
696
698
|
|
|
697
699
|
Project `routingMode` and `customGuideline` replace the global values; omission inherits them, and `customGuideline: false` disables the guideline. A project `jev` block replaces the **whole** global block, including credentials; omitted fields inside that block do not inherit. `"jev": false` disables global Jev. An invalid mode or malformed settings file disables routing. An invalid explicit Jev block disables that block instead of restoring global paid routing. Other settings changes preserve these inheritance rules.
|
|
698
700
|
|
|
699
|
-
Classifier usage is recorded separately as `routingUsage`, including in `shadow`; it does not consume coding context or workflow output budgets. Its reported cost is added once to the agent and ancestor cost totals, and `reportUsage` can return its token usage to the parent. `routing` records the mode, source, reason, applied `model` and confidence; shadow records `suggestedModel` and
|
|
701
|
+
Classifier usage is recorded separately as `routingUsage`, including in `shadow`; it does not consume coding context or workflow output budgets. Its reported cost is added once to the agent and ancestor cost totals, and `reportUsage` can return its token usage to the parent. `routing` records the mode, source, reason, applied `model`, requested `thinkingLevel` and confidence; shadow records `suggestedModel` and `suggestedThinkingLevel` and leaves both applied fields unset. `fallbackSource` identifies the default source when a suggestion is observed or Jev falls back. Shadow results show “Jev shadow”. Zero catalog prices are marked `unpriced`; the Agent result shows “Jev price unavailable” instead of treating that as proof the classifier is free.
|
|
700
702
|
|
|
701
703
|
## Model Scope
|
|
702
704
|
|
|
@@ -731,7 +733,7 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, max foreg
|
|
|
731
733
|
|
|
732
734
|
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `10`, max foreground concurrency `0` = unlimited, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, defaults enabled).
|
|
733
735
|
|
|
734
|
-
Routing settings: `routingMode` (`auto | shadow | jev | off`, default `auto`), `customGuideline` (`string | false`, default unset) and `jev` (`{ TYPESAFE_API_KEY?: string, models: { model, description }[] } | false`, default unset). See [Model routing](#model-routing) for examples and the whole-block override rule.
|
|
736
|
+
Routing settings: `routingMode` (`auto | shadow | jev | off`, default `auto`), `customGuideline` (`string | false`, default unset) and `jev` (`{ TYPESAFE_API_KEY?: string, models: { model, description, thinkingLevel }[] } | false`, default unset). See [Model routing](#model-routing) for examples and the whole-block override rule.
|
|
735
737
|
|
|
736
738
|
**Nested depth** (`maxSubagentDepth`, default `2`): the hard ceiling on [nested delegation](#nested-subagents), counted from the main session (main = 0, its subagents = 1). `0` or `1` disables nesting project-wide regardless of any agent's `allowed_subagents`. Read when a subagent session is built, so a change applies to agents started after it.
|
|
737
739
|
|
|
@@ -861,7 +863,7 @@ The four agent-lifecycle events — `subagents:started`, `:completed`, `:failed`
|
|
|
861
863
|
|
|
862
864
|
`usage` answers the other question — what was billed — and so does include `cacheRead`, because the prefix really is re-read and re-charged on every call. It is a pi `Usage`, the same shape pi puts on `ToolResultEvent` and `AssistantMessage`, so `usage.cost.total` is where a listener already expects the money and anything pi adds to `Usage` arrives without a change here. Neither field derives from the other; `tokens` is a view model, `usage` is the data.
|
|
863
865
|
|
|
864
|
-
Completed/failed payloads and persisted `subagents:record` entries also carry `routing` (`source`, `code`, `reason`, optional `model`, supplied `description`, `confidence`, `unpriced`, `guidelinePath`, `guidelineHash`) and optional `routingUsage` (classifier-only Pi `Usage`). The guideline hash is SHA-256 of the original file contents. Coding `tokens` excludes classifier tokens; total cost includes the reported classifier cost once. Settings events omit `jev.TYPESAFE_API_KEY`.
|
|
866
|
+
Completed/failed payloads and persisted `subagents:record` entries also carry `routing` (`source`, `code`, `reason`, optional `model`, `thinkingLevel`, `suggestedModel`, `suggestedThinkingLevel`, supplied `description`, `confidence`, `unpriced`, `guidelinePath`, `guidelineHash`) and optional `routingUsage` (classifier-only Pi `Usage`). The guideline hash is SHA-256 of the original file contents. Coding `tokens` excludes classifier tokens; total cost includes the reported classifier cost once. Settings events omit `jev.TYPESAFE_API_KEY`.
|
|
865
867
|
|
|
866
868
|
## Cross-Extension RPC
|
|
867
869
|
|
|
@@ -1080,7 +1082,7 @@ src/
|
|
|
1080
1082
|
invocation-config.ts # Shared tool-parameter schemas (isolation, join, thinking, ...)
|
|
1081
1083
|
model-resolver.ts # Model resolution: exact provider/modelId with fuzzy fallback
|
|
1082
1084
|
model-routing.ts # Source priority and bounded native Jev classification before startup
|
|
1083
|
-
routing-config.ts #
|
|
1085
|
+
routing-config.ts # Jev model/description/thinking-level and credential config validation
|
|
1084
1086
|
enabled-models.ts # Read pi's enabledModels settings (project over global)
|
|
1085
1087
|
model-scope.ts # scopeModels allowlist policy, shared by top-level and nested tools
|
|
1086
1088
|
mention.ts # `@handle message` grammar: suggestion triggers and send parsing
|
|
@@ -1117,6 +1119,7 @@ src/
|
|
|
1117
1119
|
agent-mention.ts # `@` roster (running, resumable, and startable agents) + popup rows
|
|
1118
1120
|
schedule-menu.ts # /agents → Scheduled jobs submenu
|
|
1119
1121
|
model-routing-menu.ts # Guideline/model descriptions and masked API-key editor
|
|
1122
|
+
routing-status.ts # Read-only effective routing configuration and retained decisions
|
|
1120
1123
|
select-item.ts # Collision-safe ctx.ui.select wrapper (numbered rows)
|
|
1121
1124
|
workflow-card.ts # Inline workflow card (tool result and session entry)
|
|
1122
1125
|
workflow-dialog.ts # /agents → Workflows two-pane inspector
|
package/dist/agent-manager.js
CHANGED
|
@@ -504,11 +504,13 @@ export class AgentManager {
|
|
|
504
504
|
});
|
|
505
505
|
record.routing = { ...routed.decision, guidelinePath: policy.guidelinePath, guidelineHash: policy.guidelineHash };
|
|
506
506
|
if (routed.model && policy.mode === "shadow") {
|
|
507
|
-
record.routing = { ...record.routing, code: "shadow", model: undefined, suggestedModel: routed.decision.model,
|
|
507
|
+
record.routing = { ...record.routing, code: "shadow", model: undefined, thinkingLevel: undefined, suggestedModel: routed.decision.model,
|
|
508
508
|
fallbackSource: policy.source, reason: "Jev suggested a model; shadow mode kept the default-priority model" };
|
|
509
509
|
}
|
|
510
510
|
else if (routed.model) {
|
|
511
511
|
options.model = routed.model;
|
|
512
|
+
options.thinkingLevel = routed.thinkingLevel;
|
|
513
|
+
record.invocation = { ...record.invocation, thinking: routed.thinkingLevel, requestedThinking: undefined };
|
|
512
514
|
}
|
|
513
515
|
}
|
|
514
516
|
catch {
|
package/dist/index.js
CHANGED
|
@@ -2752,7 +2752,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2752
2752
|
await showAgentsMenu(ctx);
|
|
2753
2753
|
}
|
|
2754
2754
|
else if (choice === "Model routing") {
|
|
2755
|
-
const patch = await showRoutingMenu(ctx);
|
|
2755
|
+
const patch = await showRoutingMenu(ctx, () => manager.listAgents());
|
|
2756
2756
|
if (patch) {
|
|
2757
2757
|
const toast = saveAndEmitChanged({ ...snapshotSettings(), ...patch }, "Model routing settings updated", (event, payload) => pi.events.emit(event, payload), ctx.cwd);
|
|
2758
2758
|
ctx.ui.notify(toast.message, toast.level);
|
package/dist/model-routing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, Model, Usage } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Api, Model, ThinkingLevel, Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { JevConfig, RoutingMode } from "./routing-config.js";
|
|
4
4
|
import type { AgentConfig } from "./types.js";
|
|
@@ -33,6 +33,8 @@ export interface RoutingDecision {
|
|
|
33
33
|
code: "baseline" | "explicit" | "off" | "shadow" | "config_unavailable" | "credentials_unavailable" | "guideline_unavailable" | "no_candidates" | "classifier_unavailable" | "cancelled" | "timeout" | "invalid_answer" | "abstained" | "unavailable_choice" | "selected" | "classifier_error";
|
|
34
34
|
model?: string;
|
|
35
35
|
suggestedModel?: string;
|
|
36
|
+
thinkingLevel?: ThinkingLevel;
|
|
37
|
+
suggestedThinkingLevel?: ThinkingLevel;
|
|
36
38
|
description?: string;
|
|
37
39
|
confidence?: number;
|
|
38
40
|
unpriced?: boolean;
|
|
@@ -42,6 +44,7 @@ export interface RoutingDecision {
|
|
|
42
44
|
export declare function loadRoutingPolicy(cwd: string, loadedAgents?: Map<string, AgentConfig>): RoutingPolicy;
|
|
43
45
|
/** Added in every description mode and refreshed before each main-agent turn. */
|
|
44
46
|
export declare function routingGuidance(policy: RoutingPolicy): string;
|
|
47
|
+
export declare function eligibleModels(ctx: ExtensionContext): Map<string, Model<Api>>;
|
|
45
48
|
/** Bounded classifier pool, independent of agent concurrency and nesting. */
|
|
46
49
|
export declare class ModelRouter {
|
|
47
50
|
private active;
|
|
@@ -49,6 +52,7 @@ export declare class ModelRouter {
|
|
|
49
52
|
private acquire;
|
|
50
53
|
choose(ctx: ExtensionContext, policy: RoutingPolicy, prompt: string, description: string, baseline: Model<Api> | undefined, signal: AbortSignal, onUsage: (usage: Usage) => void): Promise<{
|
|
51
54
|
model?: Model<Api>;
|
|
55
|
+
thinkingLevel?: ThinkingLevel;
|
|
52
56
|
decision: RoutingDecision;
|
|
53
57
|
}>;
|
|
54
58
|
}
|
package/dist/model-routing.js
CHANGED
|
@@ -59,15 +59,15 @@ export function routingGuidance(policy) {
|
|
|
59
59
|
? `Routing source: Custom guideline (${policy.guidelinePath}). Follow it when choosing Agent model/thinking (or workflow model/effort). Pass your default choice explicitly.\n<custom_routing_guideline>\n${policy.guideline}\n</custom_routing_guideline>`
|
|
60
60
|
: policy.diagnostic ?? "Custom routing guideline is unavailable. Use the existing model.";
|
|
61
61
|
break;
|
|
62
|
-
case "jev": guidance = "Routing: Jev chooses the model for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
|
|
62
|
+
case "jev": guidance = "Routing: Jev chooses the model and thinking level for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
|
|
63
63
|
}
|
|
64
64
|
if (policy.mode === "jev")
|
|
65
|
-
return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain.
|
|
65
|
+
return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain. The selected profile supplies both model and thinking level.\n" + guidance;
|
|
66
66
|
if (policy.mode === "shadow")
|
|
67
67
|
return "Routing mode: shadow. Jev records a suggestion for each fresh delegated task but never changes the model. Choose using the default guidance below, or keep the existing model when no guidance applies.\n" + guidance;
|
|
68
68
|
return guidance && policy.source !== "jev" ? guidance + "\nJev is inactive under auto mode while this routing source applies." : guidance;
|
|
69
69
|
}
|
|
70
|
-
function eligibleModels(ctx) {
|
|
70
|
+
export function eligibleModels(ctx) {
|
|
71
71
|
const scope = ctx.scopedModels;
|
|
72
72
|
const allowed = scope?.length ? new Set(scope.map(entry => `${entry.model.provider}/${entry.model.id}`)) : undefined;
|
|
73
73
|
const enabled = isScopeModelsEnabled() ? resolveEnabledModels(readEnabledModels(ctx.cwd), ctx.modelRegistry, ctx.cwd) : undefined;
|
|
@@ -139,7 +139,7 @@ export class ModelRouter {
|
|
|
139
139
|
release = await this.acquire(controller.signal);
|
|
140
140
|
if (!release)
|
|
141
141
|
return { decision: { ...decision, code: signal.aborted ? "cancelled" : "timeout", reason: signal.aborted ? "Cancelled" : "Jev timed out" } };
|
|
142
|
-
const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry
|
|
142
|
+
const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry]));
|
|
143
143
|
const criteria = { keep_baseline: "Keep the existing model if none of the described models clearly fits the task." };
|
|
144
144
|
for (const [index, entry] of candidates.entries())
|
|
145
145
|
criteria[`route_${index}`] = entry.description;
|
|
@@ -186,16 +186,19 @@ export class ModelRouter {
|
|
|
186
186
|
return { decision: { ...decision, code: "invalid_answer", reason: "Jev returned no usable choice" } };
|
|
187
187
|
}
|
|
188
188
|
decision.confidence = answer.confidence;
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
const profile = choices.get(answer.choice);
|
|
190
|
+
if (policy.mode === "shadow") {
|
|
191
|
+
decision.suggestedModel = profile?.model;
|
|
192
|
+
decision.suggestedThinkingLevel = profile?.thinkingLevel;
|
|
193
|
+
}
|
|
191
194
|
if (answer.choice === "keep_baseline" || answer.confidence < 0.6) {
|
|
192
195
|
return { decision: { ...decision, code: "abstained", reason: "Jev kept the existing model" } };
|
|
193
196
|
}
|
|
194
|
-
const selected =
|
|
197
|
+
const selected = profile?.model;
|
|
195
198
|
const model = selected ? eligibleModels(ctx).get(selected) : undefined;
|
|
196
199
|
if (!model || signal.aborted)
|
|
197
200
|
return { decision: { ...decision, code: "unavailable_choice", reason: "Selected model is no longer available in scope" } };
|
|
198
|
-
return { model, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected,
|
|
201
|
+
return { model, thinkingLevel: profile?.thinkingLevel, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected, thinkingLevel: profile?.thinkingLevel, description: profile?.description, reason: "Jev selected a configured model profile" } };
|
|
199
202
|
}
|
|
200
203
|
catch {
|
|
201
204
|
// Provider errors may contain credentials. Keep diagnostics code-owned.
|
package/dist/routing-config.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
export declare const ROUTING_THINKING_LEVELS: readonly ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1
3
|
export type RoutingMode = "auto" | "shadow" | "jev" | "off";
|
|
2
4
|
export interface JevConfig {
|
|
3
5
|
TYPESAFE_API_KEY?: string;
|
|
4
6
|
models: {
|
|
5
7
|
model: string;
|
|
6
8
|
description: string;
|
|
9
|
+
thinkingLevel: ThinkingLevel;
|
|
7
10
|
}[];
|
|
8
11
|
}
|
|
9
12
|
/** Validate the whole block; never merge candidate lists or credentials. */
|
package/dist/routing-config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const ROUTING_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1
2
|
/** Validate the whole block; never merge candidate lists or credentials. */
|
|
2
3
|
export function parseJevConfig(raw) {
|
|
3
4
|
if (raw === false)
|
|
@@ -20,15 +21,18 @@ export function parseJevConfig(raw) {
|
|
|
20
21
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
21
22
|
throw new Error("Each Jev model needs model and description");
|
|
22
23
|
const candidate = entry;
|
|
23
|
-
if (Object.keys(candidate).some(key => key !== "model" && key !== "description") ||
|
|
24
|
+
if (Object.keys(candidate).some(key => key !== "model" && key !== "description" && key !== "thinkingLevel") ||
|
|
24
25
|
typeof candidate.model !== "string" || !/^[^\s/]+\/\S+$/.test(candidate.model) ||
|
|
25
26
|
typeof candidate.description !== "string" || !candidate.description.trim() || candidate.description.length > 4000) {
|
|
26
27
|
throw new Error("Each Jev model needs an exact provider/model-id and a description of 1–4000 characters");
|
|
27
28
|
}
|
|
29
|
+
const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === candidate.thinkingLevel);
|
|
30
|
+
if (!thinkingLevel)
|
|
31
|
+
throw new Error("Each Jev model requires thinkingLevel: minimal, low, medium, high, xhigh or max");
|
|
28
32
|
if (seen.has(candidate.model))
|
|
29
33
|
throw new Error("jev.models contains duplicate models");
|
|
30
34
|
seen.add(candidate.model);
|
|
31
|
-
return { model: candidate.model, description: candidate.description.trim() };
|
|
35
|
+
return { model: candidate.model, description: candidate.description.trim(), thinkingLevel };
|
|
32
36
|
});
|
|
33
37
|
return { models, ...(typeof value.TYPESAFE_API_KEY === "string" ? { TYPESAFE_API_KEY: value.TYPESAFE_API_KEY } : {}) };
|
|
34
38
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type SubagentsSettings } from "../settings.js";
|
|
3
|
+
import type { AgentRecord } from "../types.js";
|
|
3
4
|
type RoutingSettings = Pick<SubagentsSettings, "routingMode" | "customGuideline" | "jev">;
|
|
4
5
|
/** Input handles paste/editing, but its unmasked renderer is never called. */
|
|
5
6
|
export declare function maskedApiKey(ctx: ExtensionCommandContext): Promise<string | undefined>;
|
|
6
7
|
/** Simple mode, guideline, models and credentials; saves project overrides. */
|
|
7
|
-
export declare function showRoutingMenu(ctx: ExtensionCommandContext): Promise<RoutingSettings | undefined>;
|
|
8
|
+
export declare function showRoutingMenu(ctx: ExtensionCommandContext, records?: () => AgentRecord[]): Promise<RoutingSettings | undefined>;
|
|
8
9
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Input, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { loadRoutingPolicy } from "../model-routing.js";
|
|
3
|
-
import { parseJevConfig } from "../routing-config.js";
|
|
3
|
+
import { parseJevConfig, ROUTING_THINKING_LEVELS } from "../routing-config.js";
|
|
4
4
|
import { loadSettings, projectRoutingSettings } from "../settings.js";
|
|
5
|
+
import { showRoutingStatus } from "./routing-status.js";
|
|
5
6
|
/** Input handles paste/editing, but its unmasked renderer is never called. */
|
|
6
7
|
export async function maskedApiKey(ctx) {
|
|
7
8
|
return ctx.ui.custom((_tui, _theme, _kb, done) => {
|
|
@@ -16,7 +17,7 @@ export async function maskedApiKey(ctx) {
|
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
19
|
/** Simple mode, guideline, models and credentials; saves project overrides. */
|
|
19
|
-
export async function showRoutingMenu(ctx) {
|
|
20
|
+
export async function showRoutingMenu(ctx, records = () => []) {
|
|
20
21
|
const settings = loadSettings(ctx.cwd);
|
|
21
22
|
const local = projectRoutingSettings(ctx.cwd);
|
|
22
23
|
const policy = loadRoutingPolicy(ctx.cwd);
|
|
@@ -28,10 +29,14 @@ export async function showRoutingMenu(ctx) {
|
|
|
28
29
|
: policy.mode === "jev" ? `Jev first; fallback: ${labels[policy.fallbackSource ?? "baseline"]}.${policy.jev ? "" : " Configure Jev models and credentials to use Jev."}`
|
|
29
30
|
: policy.diagnostic ?? inactive;
|
|
30
31
|
const choice = await ctx.ui.select(`Model routing: ${policy.mode} — ${note || labels[policy.source]}`, [
|
|
31
|
-
"Routing mode", "Custom guideline path", "Jev models and descriptions", "Typesafe API key", "Use Pi/environment credentials", "Disable custom guideline", "Disable Jev", "Back",
|
|
32
|
+
"Runtime status", "Routing mode", "Custom guideline path", "Jev models and descriptions", "Typesafe API key", "Use Pi/environment credentials", "Disable custom guideline", "Disable Jev", "Back",
|
|
32
33
|
]);
|
|
33
34
|
if (!choice || choice === "Back")
|
|
34
35
|
return;
|
|
36
|
+
if (choice === "Runtime status") {
|
|
37
|
+
await showRoutingStatus(ctx, records);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
35
40
|
if (choice === "Routing mode") {
|
|
36
41
|
const modes = [
|
|
37
42
|
{ mode: "auto", label: "auto — default priority: agents, guideline, Jev, existing model" },
|
|
@@ -73,7 +78,7 @@ export async function showRoutingMenu(ctx) {
|
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
for (;;) {
|
|
76
|
-
const options = [...models.map((entry, index) => `${index + 1}. ${entry.model}`), "Add model", "Save", "Cancel"];
|
|
81
|
+
const options = [...models.map((entry, index) => `${index + 1}. ${entry.model} (${entry.thinkingLevel})`), "Add model", "Save", "Cancel"];
|
|
77
82
|
const action = await ctx.ui.select("Jev models — describe which tasks each model should handle", options);
|
|
78
83
|
if (!action || action === "Cancel")
|
|
79
84
|
return;
|
|
@@ -103,7 +108,11 @@ export async function showRoutingMenu(ctx) {
|
|
|
103
108
|
const description = await ctx.ui.editor("Tasks this model is suitable for", current?.description ?? "");
|
|
104
109
|
if (!description?.trim())
|
|
105
110
|
continue;
|
|
106
|
-
const
|
|
111
|
+
const selectedLevel = await ctx.ui.select(`Thinking level (required${current ? `; current: ${current.thinkingLevel}` : ""})`, [...ROUTING_THINKING_LEVELS]);
|
|
112
|
+
const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === selectedLevel);
|
|
113
|
+
if (!thinkingLevel)
|
|
114
|
+
continue;
|
|
115
|
+
const entry = { model: model.trim(), description: description.trim(), thinkingLevel };
|
|
107
116
|
if (current)
|
|
108
117
|
models[index] = entry;
|
|
109
118
|
else
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentRecord } from "../types.js";
|
|
3
|
+
/** Read current configuration and retained decisions without making a paid classifier request. */
|
|
4
|
+
export declare function routingStatusText(ctx: ExtensionCommandContext, records: readonly AgentRecord[]): Promise<string>;
|
|
5
|
+
export declare function showRoutingStatus(ctx: ExtensionCommandContext, records: () => AgentRecord[]): Promise<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { matchesKey, Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { eligibleModels, loadRoutingPolicy } from "../model-routing.js";
|
|
3
|
+
import { isScopeModelsEnabled } from "../model-scope.js";
|
|
4
|
+
import { loadRoutingSettings, projectRoutingSettings } from "../settings.js";
|
|
5
|
+
/** Read current configuration and retained decisions without making a paid classifier request. */
|
|
6
|
+
export async function routingStatusText(ctx, records) {
|
|
7
|
+
const { settings, guidelineFile } = loadRoutingSettings(ctx.cwd);
|
|
8
|
+
const local = projectRoutingSettings(ctx.cwd);
|
|
9
|
+
const policy = loadRoutingPolicy(ctx.cwd);
|
|
10
|
+
const config = settings.jev;
|
|
11
|
+
const apiKey = config ? config.TYPESAFE_API_KEY : undefined;
|
|
12
|
+
const available = eligibleModels(ctx);
|
|
13
|
+
const labels = { agents: "Custom agents", guideline: "Custom guideline", jev: "Jev", baseline: "Existing model" };
|
|
14
|
+
const lines = [
|
|
15
|
+
`Directory: ${ctx.cwd}`,
|
|
16
|
+
`Mode: ${policy.mode} (${local.routingMode !== undefined ? "project" : settings.routingMode !== undefined ? "global" : "default"})`,
|
|
17
|
+
`Routing source: ${labels[policy.source]}`,
|
|
18
|
+
`Fallback source: ${labels[policy.fallbackSource ?? (policy.source === "jev" ? "baseline" : policy.source)]}`,
|
|
19
|
+
`Parent model: ${ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "Pi default"}`,
|
|
20
|
+
`Custom agents: ${policy.agents?.map(agent => agent.name).join(", ") || "none active in this policy"}`,
|
|
21
|
+
`Guideline: ${guidelineFile ?? "not configured"}${policy.source !== "guideline" && policy.fallbackSource !== "guideline" ? " (inactive)" : ""}`,
|
|
22
|
+
`Jev config: ${config ? "valid" : config === false ? "disabled or invalid; check subagents.json" : "not configured"} (${local.jev !== undefined ? "project" : settings.jev !== undefined ? "global" : "default"})`,
|
|
23
|
+
`Scope: Pi scoped models ${ctx.scopedModels?.length ? "active" : "unrestricted"}; enabledModels filter ${isScopeModelsEnabled() ? "on" : "off"}`,
|
|
24
|
+
];
|
|
25
|
+
if (policy.diagnostic)
|
|
26
|
+
lines.push(`Diagnostic: ${policy.diagnostic}`);
|
|
27
|
+
const classifier = ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest");
|
|
28
|
+
lines.push(`Jev classifier: ${classifier ? "available" : "unavailable in Pi registry"}`);
|
|
29
|
+
if (apiKey) {
|
|
30
|
+
lines.push("Credentials: configured key present (acceptance checked on next route)");
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
let timer;
|
|
35
|
+
try {
|
|
36
|
+
const availableCredentials = await Promise.race([
|
|
37
|
+
ctx.modelRegistry.getAvailableOfType("classifier", "typesafe", { signal: controller.signal }),
|
|
38
|
+
new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(undefined); }, 2000); }),
|
|
39
|
+
]);
|
|
40
|
+
lines.push(`Credentials: ${availableCredentials === undefined ? "check timed out" : availableCredentials.some(model => model.id === "jev-latest") ? "Pi/environment credentials available (acceptance checked on next route)" : "missing Pi/environment credentials"}`);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
lines.push("Credentials: availability check failed");
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
lines.push("", "Configured profiles:");
|
|
50
|
+
if (config) {
|
|
51
|
+
for (const profile of config.models) {
|
|
52
|
+
lines.push(`${profile.model} | thinking: ${profile.thinkingLevel} | ${available.has(profile.model) ? "eligible" : "unavailable or excluded by scope"}`, ` ${profile.description}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
lines.push("None. Every profile requires model, description and thinkingLevel.");
|
|
57
|
+
lines.push("", "Recent retained agents (latest 10; includes nested/workflow agents):");
|
|
58
|
+
const recent = [...records].filter(record => record.routing).sort((a, b) => b.startedAt - a.startedAt).slice(0, 10);
|
|
59
|
+
if (!recent.length)
|
|
60
|
+
lines.push("No routing decisions yet. Start a fresh subagent; resumed agents do not reroute.");
|
|
61
|
+
for (const record of recent) {
|
|
62
|
+
const route = record.routing;
|
|
63
|
+
lines.push(`${new Date(record.startedAt).toISOString()} | ${record.id} | ${record.status}`, ` Mode: ${route.mode} | Source: ${route.source} | Result: ${route.code}`, ` ${route.reason}`);
|
|
64
|
+
if (route.model)
|
|
65
|
+
lines.push(` Selected: ${route.model} | requested thinking: ${route.thinkingLevel ?? "inherited"}`);
|
|
66
|
+
if (route.suggestedModel)
|
|
67
|
+
lines.push(` Shadow suggestion: ${route.suggestedModel} | thinking: ${route.suggestedThinkingLevel ?? "inherited"}`);
|
|
68
|
+
if (record.invocation?.modelId)
|
|
69
|
+
lines.push(` Actual: ${record.invocation.modelId} | effective thinking: ${record.invocation.thinking ?? "unknown"}`);
|
|
70
|
+
if (route.confidence !== undefined)
|
|
71
|
+
lines.push(` Confidence: ${(route.confidence * 100).toFixed(1)}%`);
|
|
72
|
+
if (record.routingUsage)
|
|
73
|
+
lines.push(` Classifier tokens: ${record.routingUsage.totalTokens} | ${route.unpriced ? "price unavailable" : `reported cost: $${record.routingUsage.cost.total}`}`);
|
|
74
|
+
}
|
|
75
|
+
lines.push("", "Snapshot only. Refresh to reload settings and retained decisions. Opening this page does not classify a task.");
|
|
76
|
+
const text = lines.join("\n");
|
|
77
|
+
return apiKey ? text.replaceAll(apiKey, "[redacted]") : text;
|
|
78
|
+
}
|
|
79
|
+
export async function showRoutingStatus(ctx, records) {
|
|
80
|
+
let refresh = true;
|
|
81
|
+
while (refresh) {
|
|
82
|
+
const content = await routingStatusText(ctx, records());
|
|
83
|
+
refresh = await ctx.ui.custom((tui, _theme, _kb, done) => {
|
|
84
|
+
let offset = 0;
|
|
85
|
+
let maxOffset = 0;
|
|
86
|
+
return {
|
|
87
|
+
render(width) {
|
|
88
|
+
const rows = new Text(content, 0, 0).render(width);
|
|
89
|
+
const height = Math.max(1, tui.terminal.rows - 6);
|
|
90
|
+
maxOffset = Math.max(0, rows.length - height);
|
|
91
|
+
offset = Math.min(offset, maxOffset);
|
|
92
|
+
return [...new Text("Routing runtime status | Up/Down: scroll | r: refresh | Esc: back", 0, 0).render(width), ...rows.slice(offset, offset + height)];
|
|
93
|
+
},
|
|
94
|
+
invalidate() { },
|
|
95
|
+
handleInput(data) {
|
|
96
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
97
|
+
done(false);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (matchesKey(data, "r")) {
|
|
101
|
+
done(true);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (matchesKey(data, "up"))
|
|
105
|
+
offset = Math.max(0, offset - 1);
|
|
106
|
+
if (matchesKey(data, "down"))
|
|
107
|
+
offset = Math.min(maxOffset, offset + 1);
|
|
108
|
+
tui.requestRender();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
package/docs/rpc.md
CHANGED
|
@@ -55,9 +55,9 @@ Four things that are not obvious from the tables:
|
|
|
55
55
|
|
|
56
56
|
### Model routing
|
|
57
57
|
|
|
58
|
-
RPC and the manager registry follow the [same routingMode setting](../README.md#model-routing) as the tools. Under `auto` (default), enabled custom agents and a configured guideline take priority; with Jev active, a fresh spawn omitting both `model` and `thinkingLevel` is classified before worktree/session creation. Providing either field skips classification only under `auto`. Under `jev`, Jev chooses first, including over explicit or agent-file models; missing credentials, invalid configuration, uncertainty or errors keep the default-priority choice. Under `shadow`, the same check records a suggestion but preserves that choice. Under `off`, routing guidance and Jev are disabled. `null` means omitted. No mode creates an extra main-agent turn to interpret descriptions or Markdown. `awaitStartup` includes classification and its two-second deadline, while cancellation stops startup.
|
|
58
|
+
RPC and the manager registry follow the [same routingMode setting](../README.md#model-routing) as the tools. Under `auto` (default), enabled custom agents and a configured guideline take priority; with Jev active, a fresh spawn omitting both `model` and `thinkingLevel` is classified before worktree/session creation. Providing either field skips classification only under `auto`. A selected Jev profile applies its model and required `thinkingLevel` together; Pi clamps the level to model support. Shadow and fallback preserve both original values. Under `jev`, Jev chooses first, including over explicit or agent-file models; missing credentials, invalid configuration, uncertainty or errors keep the default-priority choice. Under `shadow`, the same check records a suggestion but preserves that choice. Under `off`, routing guidance and Jev are disabled. `null` means omitted. No mode creates an extra main-agent turn to interpret descriptions or Markdown. `awaitStartup` includes classification and its two-second deadline, while cancellation stops startup.
|
|
59
59
|
|
|
60
|
-
Completion events expose the credential-free `routing` decision and optional classifier-only `routingUsage`. `routing.mode` identifies the mode, `model` an applied Jev
|
|
60
|
+
Completion events expose the credential-free `routing` decision and optional classifier-only `routingUsage`. `routing.mode` identifies the mode, `model` and `thinkingLevel` an applied Jev profile, `suggestedModel` and `suggestedThinkingLevel` an observed shadow profile, and `fallbackSource` the preserved default source. Classifier tokens are separate from coding token totals; reported classifier cost, including shadow requests, is included in total cost once. `routing.unpriced` means the catalog does not provide a price. Settings events omit literal TypeSafe keys.
|
|
61
61
|
|
|
62
62
|
### Names that look right and are not
|
|
63
63
|
|
package/docs/workflows.md
CHANGED
|
@@ -308,7 +308,7 @@ A run's concurrency limit is its own, independent of the session's `maxConcurren
|
|
|
308
308
|
|
|
309
309
|
### Settings and the CLI flag
|
|
310
310
|
|
|
311
|
-
Workflow children follow [Model routing](../README.md#model-routing). With `routingMode: "auto"` (default), custom agents come first, then a main-agent Markdown guideline, then optional Jev, then the existing model. The main agent receives the guideline before writing the script and can express its choice through `agent(prompt, { model, effort })`. Either explicit option skips Jev under `auto`; workflow options retain their precedence over agent-file defaults. Under `jev`, Jev chooses first and those defaults remain the fallback on uncertainty, missing credentials or errors. Under `shadow`, Jev records a suggestion without changing the model; under `off`, routing guidance and Jev are disabled. An inherited parent model is eligible for Jev. Resuming a child does not classify again in any mode.
|
|
311
|
+
Workflow children follow [Model routing](../README.md#model-routing). With `routingMode: "auto"` (default), custom agents come first, then a main-agent Markdown guideline, then optional Jev, then the existing model. The main agent receives the guideline before writing the script and can express its choice through `agent(prompt, { model, effort })`. Either explicit option skips Jev under `auto`; workflow options retain their precedence over agent-file defaults. A selected Jev profile applies its model and required `thinkingLevel` together; Pi clamps the level to model support. Shadow and fallback preserve both original values. Under `jev`, Jev chooses first and those defaults remain the fallback on uncertainty, missing credentials or errors. Under `shadow`, Jev records a suggestion without changing the model; under `off`, routing guidance and Jev are disabled. An inherited parent model is eligible for Jev. Resuming a child does not classify again in any mode.
|
|
312
312
|
|
|
313
313
|
Jev classification happens once at child startup through Pi's native API, with a separate concurrency limit of four. Uncertainty, errors or a two-second timeout keep the existing model; stopping the workflow cancels classification without launching another child. Classifier tokens do not affect `budget.spent()` or coding context. Reported classifier cost rolls into child/ancestor cost totals once; classifier usage and the routing decision remain separately available on the agent record. Catalog-zero classifier prices mean unavailable pricing.
|
|
314
314
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diousk/pi-subagents-fast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "A pi extension that brings Claude Code-like sub-agents and workflow orchestration to pi — parallel execution, live widget, fleet view, custom agent types, mid-run steering, dynamic workflows, Claude Code compatibility, look and feel.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"license": "MIT",
|
package/src/agent-manager.ts
CHANGED
|
@@ -742,10 +742,12 @@ export class AgentManager {
|
|
|
742
742
|
});
|
|
743
743
|
record.routing = { ...routed.decision, guidelinePath: policy.guidelinePath, guidelineHash: policy.guidelineHash };
|
|
744
744
|
if (routed.model && policy.mode === "shadow") {
|
|
745
|
-
record.routing = { ...record.routing, code: "shadow", model: undefined, suggestedModel: routed.decision.model,
|
|
745
|
+
record.routing = { ...record.routing, code: "shadow", model: undefined, thinkingLevel: undefined, suggestedModel: routed.decision.model,
|
|
746
746
|
fallbackSource: policy.source, reason: "Jev suggested a model; shadow mode kept the default-priority model" };
|
|
747
747
|
} else if (routed.model) {
|
|
748
748
|
options.model = routed.model;
|
|
749
|
+
options.thinkingLevel = routed.thinkingLevel;
|
|
750
|
+
record.invocation = { ...record.invocation, thinking: routed.thinkingLevel, requestedThinking: undefined };
|
|
749
751
|
}
|
|
750
752
|
} catch {
|
|
751
753
|
record.routing.code = "classifier_error";
|
package/src/index.ts
CHANGED
|
@@ -2994,7 +2994,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2994
2994
|
await showSettings(ctx);
|
|
2995
2995
|
await showAgentsMenu(ctx);
|
|
2996
2996
|
} else if (choice === "Model routing") {
|
|
2997
|
-
const patch = await showRoutingMenu(ctx);
|
|
2997
|
+
const patch = await showRoutingMenu(ctx, () => manager.listAgents());
|
|
2998
2998
|
if (patch) {
|
|
2999
2999
|
const toast = saveAndEmitChanged({ ...snapshotSettings(), ...patch }, "Model routing settings updated", (event, payload) => pi.events.emit(event, payload), ctx.cwd);
|
|
3000
3000
|
ctx.ui.notify(toast.message, toast.level);
|
package/src/model-routing.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync, statSync } from "node:fs";
|
|
3
|
-
import type { Api, ClassifierResult, Model, Usage } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { Api, ClassifierResult, Model, ThinkingLevel, Usage } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
6
6
|
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
@@ -40,6 +40,8 @@ export interface RoutingDecision {
|
|
|
40
40
|
"cancelled" | "timeout" | "invalid_answer" | "abstained" | "unavailable_choice" | "selected" | "classifier_error";
|
|
41
41
|
model?: string;
|
|
42
42
|
suggestedModel?: string;
|
|
43
|
+
thinkingLevel?: ThinkingLevel;
|
|
44
|
+
suggestedThinkingLevel?: ThinkingLevel;
|
|
43
45
|
description?: string;
|
|
44
46
|
confidence?: number;
|
|
45
47
|
unpriced?: boolean;
|
|
@@ -93,14 +95,14 @@ export function routingGuidance(policy: RoutingPolicy): string {
|
|
|
93
95
|
? `Routing source: Custom guideline (${policy.guidelinePath}). Follow it when choosing Agent model/thinking (or workflow model/effort). Pass your default choice explicitly.\n<custom_routing_guideline>\n${policy.guideline}\n</custom_routing_guideline>`
|
|
94
96
|
: policy.diagnostic ?? "Custom routing guideline is unavailable. Use the existing model.";
|
|
95
97
|
break;
|
|
96
|
-
case "jev": guidance = "Routing: Jev chooses the model for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
|
|
98
|
+
case "jev": guidance = "Routing: Jev chooses the model and thinking level for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
|
|
97
99
|
}
|
|
98
|
-
if (policy.mode === "jev") return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain.
|
|
100
|
+
if (policy.mode === "jev") return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain. The selected profile supplies both model and thinking level.\n" + guidance;
|
|
99
101
|
if (policy.mode === "shadow") return "Routing mode: shadow. Jev records a suggestion for each fresh delegated task but never changes the model. Choose using the default guidance below, or keep the existing model when no guidance applies.\n" + guidance;
|
|
100
102
|
return guidance && policy.source !== "jev" ? guidance + "\nJev is inactive under auto mode while this routing source applies." : guidance;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
function eligibleModels(ctx: ExtensionContext): Map<string, Model<Api>> {
|
|
105
|
+
export function eligibleModels(ctx: ExtensionContext): Map<string, Model<Api>> {
|
|
104
106
|
const scope = ctx.scopedModels;
|
|
105
107
|
const allowed = scope?.length ? new Set(scope.map(entry => `${entry.model.provider}/${entry.model.id}`)) : undefined;
|
|
106
108
|
const enabled = isScopeModelsEnabled() ? resolveEnabledModels(readEnabledModels(ctx.cwd), ctx.modelRegistry, ctx.cwd) : undefined;
|
|
@@ -148,7 +150,7 @@ export class ModelRouter {
|
|
|
148
150
|
baseline: Model<Api> | undefined,
|
|
149
151
|
signal: AbortSignal,
|
|
150
152
|
onUsage: (usage: Usage) => void,
|
|
151
|
-
): Promise<{ model?: Model<Api>; decision: RoutingDecision }> {
|
|
153
|
+
): Promise<{ model?: Model<Api>; thinkingLevel?: ThinkingLevel; decision: RoutingDecision }> {
|
|
152
154
|
const decision: RoutingDecision = { mode: policy.mode, source: "jev", fallbackSource: policy.fallbackSource ?? (policy.source === "jev" ? "baseline" : policy.source), code: "baseline", reason: "Using the default-priority model" };
|
|
153
155
|
const config = policy.jev;
|
|
154
156
|
if (policy.mode === "off" || (policy.mode === "auto" && policy.source !== "jev")) return { decision: { ...decision, source: policy.source } };
|
|
@@ -169,7 +171,7 @@ export class ModelRouter {
|
|
|
169
171
|
try {
|
|
170
172
|
release = await this.acquire(controller.signal);
|
|
171
173
|
if (!release) return { decision: { ...decision, code: signal.aborted ? "cancelled" : "timeout", reason: signal.aborted ? "Cancelled" : "Jev timed out" } };
|
|
172
|
-
const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry
|
|
174
|
+
const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry]));
|
|
173
175
|
const criteria: Record<string, string> = { keep_baseline: "Keep the existing model if none of the described models clearly fits the task." };
|
|
174
176
|
for (const [index, entry] of candidates.entries()) criteria[`route_${index}`] = entry.description;
|
|
175
177
|
const cancelled = new Promise<undefined>(resolve => {
|
|
@@ -208,14 +210,18 @@ export class ModelRouter {
|
|
|
208
210
|
return { decision: { ...decision, code: "invalid_answer", reason: "Jev returned no usable choice" } };
|
|
209
211
|
}
|
|
210
212
|
decision.confidence = answer.confidence;
|
|
211
|
-
|
|
213
|
+
const profile = choices.get(answer.choice);
|
|
214
|
+
if (policy.mode === "shadow") {
|
|
215
|
+
decision.suggestedModel = profile?.model;
|
|
216
|
+
decision.suggestedThinkingLevel = profile?.thinkingLevel;
|
|
217
|
+
}
|
|
212
218
|
if (answer.choice === "keep_baseline" || answer.confidence < 0.6) {
|
|
213
219
|
return { decision: { ...decision, code: "abstained", reason: "Jev kept the existing model" } };
|
|
214
220
|
}
|
|
215
|
-
const selected =
|
|
221
|
+
const selected = profile?.model;
|
|
216
222
|
const model = selected ? eligibleModels(ctx).get(selected) : undefined;
|
|
217
223
|
if (!model || signal.aborted) return { decision: { ...decision, code: "unavailable_choice", reason: "Selected model is no longer available in scope" } };
|
|
218
|
-
return { model, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected,
|
|
224
|
+
return { model, thinkingLevel: profile?.thinkingLevel, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected, thinkingLevel: profile?.thinkingLevel, description: profile?.description, reason: "Jev selected a configured model profile" } };
|
|
219
225
|
} catch {
|
|
220
226
|
// Provider errors may contain credentials. Keep diagnostics code-owned.
|
|
221
227
|
return { decision: { ...decision, code: "classifier_error", reason: "Jev could not choose a model; using the existing model" } };
|
package/src/routing-config.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export const ROUTING_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
|
|
4
|
+
|
|
1
5
|
export type RoutingMode = "auto" | "shadow" | "jev" | "off";
|
|
2
6
|
|
|
3
7
|
export interface JevConfig {
|
|
4
8
|
TYPESAFE_API_KEY?: string;
|
|
5
|
-
models: { model: string; description: string }[];
|
|
9
|
+
models: { model: string; description: string; thinkingLevel: ThinkingLevel }[];
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
/** Validate the whole block; never merge candidate lists or credentials. */
|
|
@@ -24,14 +28,16 @@ export function parseJevConfig(raw: unknown): JevConfig | false {
|
|
|
24
28
|
const models = value.models.map((entry: unknown) => {
|
|
25
29
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error("Each Jev model needs model and description");
|
|
26
30
|
const candidate = entry as Record<string, unknown>;
|
|
27
|
-
if (Object.keys(candidate).some(key => key !== "model" && key !== "description") ||
|
|
31
|
+
if (Object.keys(candidate).some(key => key !== "model" && key !== "description" && key !== "thinkingLevel") ||
|
|
28
32
|
typeof candidate.model !== "string" || !/^[^\s/]+\/\S+$/.test(candidate.model) ||
|
|
29
33
|
typeof candidate.description !== "string" || !candidate.description.trim() || candidate.description.length > 4000) {
|
|
30
34
|
throw new Error("Each Jev model needs an exact provider/model-id and a description of 1–4000 characters");
|
|
31
35
|
}
|
|
36
|
+
const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === candidate.thinkingLevel);
|
|
37
|
+
if (!thinkingLevel) throw new Error("Each Jev model requires thinkingLevel: minimal, low, medium, high, xhigh or max");
|
|
32
38
|
if (seen.has(candidate.model)) throw new Error("jev.models contains duplicate models");
|
|
33
39
|
seen.add(candidate.model);
|
|
34
|
-
return { model: candidate.model, description: candidate.description.trim() };
|
|
40
|
+
return { model: candidate.model, description: candidate.description.trim(), thinkingLevel };
|
|
35
41
|
});
|
|
36
42
|
return { models, ...(typeof value.TYPESAFE_API_KEY === "string" ? { TYPESAFE_API_KEY: value.TYPESAFE_API_KEY } : {}) };
|
|
37
43
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Input, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { loadRoutingPolicy } from "../model-routing.js";
|
|
4
|
-
import { parseJevConfig, type RoutingMode } from "../routing-config.js";
|
|
4
|
+
import { parseJevConfig, ROUTING_THINKING_LEVELS, type RoutingMode } from "../routing-config.js";
|
|
5
5
|
import { loadSettings, projectRoutingSettings, type SubagentsSettings } from "../settings.js";
|
|
6
|
+
import type { AgentRecord } from "../types.js";
|
|
7
|
+
import { showRoutingStatus } from "./routing-status.js";
|
|
6
8
|
|
|
7
9
|
type RoutingSettings = Pick<SubagentsSettings, "routingMode" | "customGuideline" | "jev">;
|
|
8
10
|
|
|
@@ -21,7 +23,7 @@ export async function maskedApiKey(ctx: ExtensionCommandContext): Promise<string
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** Simple mode, guideline, models and credentials; saves project overrides. */
|
|
24
|
-
export async function showRoutingMenu(ctx: ExtensionCommandContext): Promise<RoutingSettings | undefined> {
|
|
26
|
+
export async function showRoutingMenu(ctx: ExtensionCommandContext, records: () => AgentRecord[] = () => []): Promise<RoutingSettings | undefined> {
|
|
25
27
|
const settings = loadSettings(ctx.cwd);
|
|
26
28
|
const local = projectRoutingSettings(ctx.cwd);
|
|
27
29
|
const policy = loadRoutingPolicy(ctx.cwd);
|
|
@@ -33,9 +35,13 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext): Promise<Rou
|
|
|
33
35
|
: policy.mode === "jev" ? `Jev first; fallback: ${labels[policy.fallbackSource ?? "baseline"]}.${policy.jev ? "" : " Configure Jev models and credentials to use Jev."}`
|
|
34
36
|
: policy.diagnostic ?? inactive;
|
|
35
37
|
const choice = await ctx.ui.select(`Model routing: ${policy.mode} — ${note || labels[policy.source]}`, [
|
|
36
|
-
"Routing mode", "Custom guideline path", "Jev models and descriptions", "Typesafe API key", "Use Pi/environment credentials", "Disable custom guideline", "Disable Jev", "Back",
|
|
38
|
+
"Runtime status", "Routing mode", "Custom guideline path", "Jev models and descriptions", "Typesafe API key", "Use Pi/environment credentials", "Disable custom guideline", "Disable Jev", "Back",
|
|
37
39
|
]);
|
|
38
40
|
if (!choice || choice === "Back") return;
|
|
41
|
+
if (choice === "Runtime status") {
|
|
42
|
+
await showRoutingStatus(ctx, records);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
39
45
|
if (choice === "Routing mode") {
|
|
40
46
|
const modes: { mode: RoutingMode; label: string }[] = [
|
|
41
47
|
{ mode: "auto", label: "auto — default priority: agents, guideline, Jev, existing model" },
|
|
@@ -65,7 +71,7 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext): Promise<Rou
|
|
|
65
71
|
catch (err) { ctx.ui.notify(err instanceof Error ? err.message : "Invalid Jev settings", "warning"); return; }
|
|
66
72
|
}
|
|
67
73
|
for (;;) {
|
|
68
|
-
const options = [...models.map((entry, index) => `${index + 1}. ${entry.model}`), "Add model", "Save", "Cancel"];
|
|
74
|
+
const options = [...models.map((entry, index) => `${index + 1}. ${entry.model} (${entry.thinkingLevel})`), "Add model", "Save", "Cancel"];
|
|
69
75
|
const action = await ctx.ui.select("Jev models — describe which tasks each model should handle", options);
|
|
70
76
|
if (!action || action === "Cancel") return;
|
|
71
77
|
if (action === "Save") {
|
|
@@ -83,7 +89,10 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext): Promise<Rou
|
|
|
83
89
|
if (!model) continue;
|
|
84
90
|
const description = await ctx.ui.editor("Tasks this model is suitable for", current?.description ?? "");
|
|
85
91
|
if (!description?.trim()) continue;
|
|
86
|
-
const
|
|
92
|
+
const selectedLevel = await ctx.ui.select(`Thinking level (required${current ? `; current: ${current.thinkingLevel}` : ""})`, [...ROUTING_THINKING_LEVELS]);
|
|
93
|
+
const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === selectedLevel);
|
|
94
|
+
if (!thinkingLevel) continue;
|
|
95
|
+
const entry = { model: model.trim(), description: description.trim(), thinkingLevel };
|
|
87
96
|
if (current) models[index] = entry;
|
|
88
97
|
else models.push(entry);
|
|
89
98
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { matchesKey, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { eligibleModels, loadRoutingPolicy } from "../model-routing.js";
|
|
4
|
+
import { isScopeModelsEnabled } from "../model-scope.js";
|
|
5
|
+
import { loadRoutingSettings, projectRoutingSettings } from "../settings.js";
|
|
6
|
+
import type { AgentRecord } from "../types.js";
|
|
7
|
+
|
|
8
|
+
/** Read current configuration and retained decisions without making a paid classifier request. */
|
|
9
|
+
export async function routingStatusText(ctx: ExtensionCommandContext, records: readonly AgentRecord[]): Promise<string> {
|
|
10
|
+
const { settings, guidelineFile } = loadRoutingSettings(ctx.cwd);
|
|
11
|
+
const local = projectRoutingSettings(ctx.cwd);
|
|
12
|
+
const policy = loadRoutingPolicy(ctx.cwd);
|
|
13
|
+
const config = settings.jev;
|
|
14
|
+
const apiKey = config ? config.TYPESAFE_API_KEY : undefined;
|
|
15
|
+
const available = eligibleModels(ctx);
|
|
16
|
+
const labels = { agents: "Custom agents", guideline: "Custom guideline", jev: "Jev", baseline: "Existing model" };
|
|
17
|
+
const lines = [
|
|
18
|
+
`Directory: ${ctx.cwd}`,
|
|
19
|
+
`Mode: ${policy.mode} (${local.routingMode !== undefined ? "project" : settings.routingMode !== undefined ? "global" : "default"})`,
|
|
20
|
+
`Routing source: ${labels[policy.source]}`,
|
|
21
|
+
`Fallback source: ${labels[policy.fallbackSource ?? (policy.source === "jev" ? "baseline" : policy.source)]}`,
|
|
22
|
+
`Parent model: ${ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "Pi default"}`,
|
|
23
|
+
`Custom agents: ${policy.agents?.map(agent => agent.name).join(", ") || "none active in this policy"}`,
|
|
24
|
+
`Guideline: ${guidelineFile ?? "not configured"}${policy.source !== "guideline" && policy.fallbackSource !== "guideline" ? " (inactive)" : ""}`,
|
|
25
|
+
`Jev config: ${config ? "valid" : config === false ? "disabled or invalid; check subagents.json" : "not configured"} (${local.jev !== undefined ? "project" : settings.jev !== undefined ? "global" : "default"})`,
|
|
26
|
+
`Scope: Pi scoped models ${ctx.scopedModels?.length ? "active" : "unrestricted"}; enabledModels filter ${isScopeModelsEnabled() ? "on" : "off"}`,
|
|
27
|
+
];
|
|
28
|
+
if (policy.diagnostic) lines.push(`Diagnostic: ${policy.diagnostic}`);
|
|
29
|
+
const classifier = ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest");
|
|
30
|
+
lines.push(`Jev classifier: ${classifier ? "available" : "unavailable in Pi registry"}`);
|
|
31
|
+
if (apiKey) {
|
|
32
|
+
lines.push("Credentials: configured key present (acceptance checked on next route)");
|
|
33
|
+
} else {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
+
try {
|
|
37
|
+
const availableCredentials = await Promise.race([
|
|
38
|
+
ctx.modelRegistry.getAvailableOfType("classifier", "typesafe", { signal: controller.signal }),
|
|
39
|
+
new Promise<undefined>(resolve => { timer = setTimeout(() => { controller.abort(); resolve(undefined); }, 2000); }),
|
|
40
|
+
]);
|
|
41
|
+
lines.push(`Credentials: ${availableCredentials === undefined ? "check timed out" : availableCredentials.some(model => model.id === "jev-latest") ? "Pi/environment credentials available (acceptance checked on next route)" : "missing Pi/environment credentials"}`);
|
|
42
|
+
} catch {
|
|
43
|
+
lines.push("Credentials: availability check failed");
|
|
44
|
+
} finally { clearTimeout(timer); }
|
|
45
|
+
}
|
|
46
|
+
lines.push("", "Configured profiles:");
|
|
47
|
+
if (config) {
|
|
48
|
+
for (const profile of config.models) {
|
|
49
|
+
lines.push(`${profile.model} | thinking: ${profile.thinkingLevel} | ${available.has(profile.model) ? "eligible" : "unavailable or excluded by scope"}`, ` ${profile.description}`);
|
|
50
|
+
}
|
|
51
|
+
} else lines.push("None. Every profile requires model, description and thinkingLevel.");
|
|
52
|
+
lines.push("", "Recent retained agents (latest 10; includes nested/workflow agents):");
|
|
53
|
+
const recent = [...records].filter(record => record.routing).sort((a, b) => b.startedAt - a.startedAt).slice(0, 10);
|
|
54
|
+
if (!recent.length) lines.push("No routing decisions yet. Start a fresh subagent; resumed agents do not reroute.");
|
|
55
|
+
for (const record of recent) {
|
|
56
|
+
const route = record.routing!;
|
|
57
|
+
lines.push(`${new Date(record.startedAt).toISOString()} | ${record.id} | ${record.status}`,
|
|
58
|
+
` Mode: ${route.mode} | Source: ${route.source} | Result: ${route.code}`,
|
|
59
|
+
` ${route.reason}`);
|
|
60
|
+
if (route.model) lines.push(` Selected: ${route.model} | requested thinking: ${route.thinkingLevel ?? "inherited"}`);
|
|
61
|
+
if (route.suggestedModel) lines.push(` Shadow suggestion: ${route.suggestedModel} | thinking: ${route.suggestedThinkingLevel ?? "inherited"}`);
|
|
62
|
+
if (record.invocation?.modelId) lines.push(` Actual: ${record.invocation.modelId} | effective thinking: ${record.invocation.thinking ?? "unknown"}`);
|
|
63
|
+
if (route.confidence !== undefined) lines.push(` Confidence: ${(route.confidence * 100).toFixed(1)}%`);
|
|
64
|
+
if (record.routingUsage) lines.push(` Classifier tokens: ${record.routingUsage.totalTokens} | ${route.unpriced ? "price unavailable" : `reported cost: $${record.routingUsage.cost.total}`}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push("", "Snapshot only. Refresh to reload settings and retained decisions. Opening this page does not classify a task.");
|
|
67
|
+
const text = lines.join("\n");
|
|
68
|
+
return apiKey ? text.replaceAll(apiKey, "[redacted]") : text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function showRoutingStatus(ctx: ExtensionCommandContext, records: () => AgentRecord[]): Promise<void> {
|
|
72
|
+
let refresh = true;
|
|
73
|
+
while (refresh) {
|
|
74
|
+
const content = await routingStatusText(ctx, records());
|
|
75
|
+
refresh = await ctx.ui.custom<boolean>((tui, _theme, _kb, done) => {
|
|
76
|
+
let offset = 0;
|
|
77
|
+
let maxOffset = 0;
|
|
78
|
+
return {
|
|
79
|
+
render(width: number) {
|
|
80
|
+
const rows = new Text(content, 0, 0).render(width);
|
|
81
|
+
const height = Math.max(1, tui.terminal.rows - 6);
|
|
82
|
+
maxOffset = Math.max(0, rows.length - height);
|
|
83
|
+
offset = Math.min(offset, maxOffset);
|
|
84
|
+
return [...new Text("Routing runtime status | Up/Down: scroll | r: refresh | Esc: back", 0, 0).render(width), ...rows.slice(offset, offset + height)];
|
|
85
|
+
},
|
|
86
|
+
invalidate() {},
|
|
87
|
+
handleInput(data: string) {
|
|
88
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q")) { done(false); return; }
|
|
89
|
+
if (matchesKey(data, "r")) { done(true); return; }
|
|
90
|
+
if (matchesKey(data, "up")) offset = Math.max(0, offset - 1);
|
|
91
|
+
if (matchesKey(data, "down")) offset = Math.min(maxOffset, offset + 1);
|
|
92
|
+
tui.requestRender();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|