pi-advisor-flow 0.9.2-dev.3 → 0.9.2-dev.5

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -2
  3. package/dist/index.js +580 -201
  4. package/package.json +1 -1
  5. package/src/commands/manual-consultation.ts +31 -26
  6. package/src/commands/outcome-stats.ts +98 -0
  7. package/src/commands/registration.ts +2 -0
  8. package/src/commands/renderers.ts +51 -0
  9. package/src/commands/runtime.ts +2 -3
  10. package/src/commands/settings-persistence.ts +16 -7
  11. package/src/commands/types.ts +3 -2
  12. package/src/config/args.ts +3 -1
  13. package/src/config/defaults.ts +7 -0
  14. package/src/config/schema.ts +7 -0
  15. package/src/config/state.ts +5 -0
  16. package/src/config/storage.ts +23 -2
  17. package/src/config/types.ts +1 -0
  18. package/src/config.ts +2 -0
  19. package/src/image-attachments.ts +7 -3
  20. package/src/outcome-stats.ts +154 -0
  21. package/src/outcomes.ts +8 -1
  22. package/src/preferences.ts +103 -13
  23. package/src/redaction.ts +1 -1
  24. package/src/tools/consult-context.ts +12 -1
  25. package/src/tools/consultation.ts +34 -7
  26. package/src/tools/jev-filter.ts +2 -1
  27. package/src/tools/jev-turn-gate.ts +2 -0
  28. package/src/tools/loop-gate.ts +2 -0
  29. package/src/tools/prompts.ts +28 -2
  30. package/src/tools/register-ask-advisor.ts +1 -0
  31. package/src/tools/register-renderers.ts +40 -6
  32. package/src/tools/registration.ts +2 -2
  33. package/src/tools/render-advisor-result.ts +21 -11
  34. package/src/tools/session.ts +14 -0
  35. package/src/tools/types.ts +3 -0
  36. package/src/ui/manual-dialog.ts +10 -3
  37. package/src/ui/settings-items.ts +7 -0
  38. package/src/ui/settings-mutations.ts +1 -0
  39. package/src/ui/types.ts +1 -0
  40. package/src/usage.ts +34 -6
@@ -24,12 +24,27 @@ const advisorResultDetails = (result: AgentToolResult<AdvisorToolDetails>) =>
24
24
 
25
25
  const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
26
26
  if (context.state.phase !== phase && context.state.timerId) {
27
- clearInterval(context.state.timerId);
27
+ clearTimeout(context.state.timerId);
28
28
  context.state.timerId = undefined;
29
29
  }
30
30
  context.state.phase = phase;
31
31
  };
32
32
 
33
+ const scheduleRender = (context: AdvisorToolContext) => {
34
+ const timer = setTimeout(() => {
35
+ if (context.state.timerId !== timer) {
36
+ return;
37
+ }
38
+ context.state.timerId = undefined;
39
+ try {
40
+ context.invalidate();
41
+ } catch {
42
+ // The render slot may have been discarded before the repaint fired.
43
+ }
44
+ }, 80);
45
+ context.state.timerId = timer;
46
+ };
47
+
33
48
  const formatByteSize = (bytes: number): string => {
34
49
  if (bytes >= 1024 ** 3) {
35
50
  return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
@@ -45,6 +60,9 @@ const formatByteSize = (bytes: number): string => {
45
60
 
46
61
  const attachmentLabels = (details: AdvisorToolDetails | undefined) =>
47
62
  [
63
+ details?.agentRulesBytes
64
+ ? `AGENTS.md rules attached · ${formatByteSize(details.agentRulesBytes)}`
65
+ : undefined,
48
66
  details?.draftBytes
49
67
  ? `Draft attached · ${formatByteSize(details.draftBytes)}`
50
68
  : undefined,
@@ -100,15 +118,7 @@ const renderPartialAdvisorResult = (
100
118
  scout?.status === "calling" || scout?.status === "streaming";
101
119
  syncRenderPhase(context, scoutActive ? "scout" : "advisor");
102
120
  if (!context.state.timerId) {
103
- context.state.timerId = setInterval(() => {
104
- try {
105
- context.invalidate();
106
- } catch {
107
- // Renderer context is gone; stop repainting and drop the timer.
108
- clearInterval(context.state.timerId);
109
- context.state.timerId = undefined;
110
- }
111
- }, 80);
121
+ scheduleRender(context);
112
122
  }
113
123
  if (scout) {
114
124
  renderScoutDetails(box, scout, expanded, theme);
@@ -177,7 +187,7 @@ const renderFinalAdvisorResult = (
177
187
  ) => {
178
188
  syncRenderPhase(context, "final");
179
189
  if (context.state.timerId) {
180
- clearInterval(context.state.timerId);
190
+ clearTimeout(context.state.timerId);
181
191
  context.state.timerId = undefined;
182
192
  }
183
193
  const details = advisorResultDetails(result);
@@ -1,3 +1,17 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
1
3
  import { AdvisorSessionState } from "../session-state.ts";
2
4
 
3
5
  export const advisorSessionState = new AdvisorSessionState();
6
+
7
+ const sessionStates = new WeakMap<object, AdvisorSessionState>();
8
+
9
+ export const sessionStateFor = (owner: ExtensionAPI): AdvisorSessionState => {
10
+ const existing = sessionStates.get(owner);
11
+ if (existing) {
12
+ return existing;
13
+ }
14
+ const state = new AdvisorSessionState();
15
+ sessionStates.set(owner, state);
16
+ return state;
17
+ };
@@ -46,6 +46,7 @@ export interface AdvisorGateFailure {
46
46
 
47
47
  export interface AdvisorConsultationResult {
48
48
  adviceId: string;
49
+ agentRulesBytes?: number;
49
50
  draftBytes?: number;
50
51
  imageBytes?: number;
51
52
  imageCount?: number;
@@ -63,6 +64,7 @@ export interface AdvisorConsultationResult {
63
64
  }
64
65
 
65
66
  export interface AdvisorGateResult {
67
+ agentRulesBytes?: number;
66
68
  decision: GateDecision;
67
69
  markdown: string;
68
70
  model: string;
@@ -86,6 +88,7 @@ export type GateFailureCategory =
86
88
  export interface AdvisorToolDetails {
87
89
  adviceId?: string;
88
90
  advisor?: string;
91
+ agentRulesBytes?: number;
89
92
  draftBytes?: number;
90
93
  imageBytes?: number;
91
94
  imageCount?: number;
@@ -24,6 +24,7 @@ const TUI_INPUT_TAB: keyof Keybindings = ["tui", "input", "tab"].join(
24
24
  interface ManualDialogState {
25
25
  completed: boolean;
26
26
  focused: boolean;
27
+ submitting: boolean;
27
28
  }
28
29
 
29
30
  const isShiftTab = (keyData: string): boolean =>
@@ -40,6 +41,7 @@ export class ManualAdvisorDialog implements Component, Focusable {
40
41
  private readonly state: ManualDialogState = {
41
42
  completed: false,
42
43
  focused: false,
44
+ submitting: false,
43
45
  };
44
46
 
45
47
  get focused(): boolean {
@@ -273,17 +275,22 @@ export class ManualAdvisorDialog implements Component, Focusable {
273
275
  }
274
276
 
275
277
  private submit(message = this.editor.getText()): void {
276
- if (this.state.completed) {
278
+ if (this.state.completed || this.state.submitting) {
277
279
  return;
278
280
  }
279
- this.state.completed = true;
281
+ this.state.submitting = true;
280
282
  const request: ManualAdvisorRequest = {
281
283
  gitContext: this.gitLevels[this.gitIndex] ?? "off",
282
284
  };
283
285
  if (message) {
284
286
  request.message = message;
285
287
  }
286
- this.options.onSubmit(request);
288
+ try {
289
+ this.options.onSubmit(request);
290
+ this.state.completed = true;
291
+ } finally {
292
+ this.state.submitting = false;
293
+ }
287
294
  }
288
295
 
289
296
  private cancel(): void {
@@ -472,6 +472,13 @@ export const createSettingsItems = ({
472
472
  settings.redactSecrets,
473
473
  false
474
474
  ),
475
+ toggle(
476
+ "agentsMdContext",
477
+ "AGENTS.md context",
478
+ "Include trusted project and global AGENTS.md rules in Advisor calls.",
479
+ settings.agentsMdContext,
480
+ true
481
+ ),
475
482
  {
476
483
  currentValue: settings.gitContext ?? "summary",
477
484
  description: "How much repository context is shared with the Advisor.",
@@ -2,6 +2,7 @@ import { DEFAULT_JEV_MODEL } from "../config/types.ts";
2
2
  import type { AdvisorSettings, ContextPreset } from "./types.ts";
3
3
 
4
4
  const BOOLEAN_SETTING_FIELDS = [
5
+ "agentsMdContext",
5
6
  "autoLoopGate",
6
7
  "blockOnBlocked",
7
8
  "collapseResponses",
package/src/ui/types.ts CHANGED
@@ -67,6 +67,7 @@ export interface ContextPreset {
67
67
  }
68
68
 
69
69
  export interface AdvisorSettings {
70
+ agentsMdContext?: boolean;
70
71
  alwaysOn?: boolean;
71
72
  autoLoopGate?: boolean;
72
73
  blockOnBlocked?: boolean;
package/src/usage.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai/compat";
2
2
 
3
3
  import { isNumber, isRecordOf } from "./content-utils.ts";
4
- import type { JsonValue } from "./content-utils.ts";
4
+ import type { JsonValue, RecordValue } from "./content-utils.ts";
5
5
 
6
6
  /** Normalized usage returned by an Advisor or Scout provider response. */
7
7
  export interface AdvisorUsageSnapshot {
@@ -33,6 +33,18 @@ const add = (left: number | undefined, right: number | undefined) =>
33
33
 
34
34
  const costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
35
35
 
36
+ const hasPartialProviderCost = (
37
+ snapshot: AdvisorUsageSnapshot,
38
+ usage: RecordValue
39
+ ) => {
40
+ const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
41
+ return (
42
+ cost !== undefined &&
43
+ snapshot.cost === undefined &&
44
+ costFields.some((field) => finite(cost[field]) !== undefined)
45
+ );
46
+ };
47
+
36
48
  /** Extracts provider-agnostic usage fields without trusting provider metadata. */
37
49
  export const snapshotAdvisorUsage = <Input>(
38
50
  usage: Input
@@ -62,23 +74,39 @@ export const snapshotAdvisorUsage = <Input>(
62
74
  export const advisorUsageCost = <Input>(usage: Input): number | undefined =>
63
75
  snapshotAdvisorUsage(usage)?.cost;
64
76
 
77
+ const completeUsageSnapshot = <Input>(
78
+ usage: Input
79
+ ): { snapshot: AdvisorUsageSnapshot; usage: RecordValue } | undefined => {
80
+ const snapshot = snapshotAdvisorUsage(usage);
81
+ if (!(snapshot && isRecordOf(usage))) {
82
+ return;
83
+ }
84
+ if (hasPartialProviderCost(snapshot, usage)) {
85
+ return;
86
+ }
87
+ return { snapshot, usage };
88
+ };
89
+
65
90
  /**
66
91
  * Converts supported provider usage to Pi's complete nested-tool usage shape.
67
92
  * Missing fields become zero only at this Pi API boundary; absent usage remains
68
93
  * undefined so an unavailable request is never presented as a zero-cost call.
69
94
  */
70
95
  export const advisorUsageForPi = <Input>(usage: Input): Usage | undefined => {
71
- const snapshot = snapshotAdvisorUsage(usage);
72
- if (!(snapshot && isRecordOf(usage))) {
96
+ const complete = completeUsageSnapshot(usage);
97
+ if (!complete) {
73
98
  return undefined;
74
99
  }
75
- const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
100
+ const { snapshot } = complete;
101
+ const cost = isRecordOf(complete.usage.cost)
102
+ ? complete.usage.cost
103
+ : undefined;
76
104
  const input = snapshot.input ?? 0;
77
105
  const output = snapshot.output ?? 0;
78
106
  const cacheRead = snapshot.cacheRead ?? 0;
79
107
  const cacheWrite = snapshot.cacheWrite ?? 0;
80
- const cacheWrite1h = finite(usage.cacheWrite1h);
81
- const reasoning = finite(usage.reasoning);
108
+ const cacheWrite1h = finite(complete.usage.cacheWrite1h);
109
+ const reasoning = finite(complete.usage.reasoning);
82
110
  const piUsage: Usage = {
83
111
  cacheRead,
84
112
  cacheWrite,