pi-ghost-autocomplete 0.9.1 → 0.11.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.
@@ -5,42 +5,14 @@
5
5
  import { CustomEditor, type ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
7
7
  import { type EditorTheme, type TUI } from "@earendil-works/pi-tui";
8
- import type { AcceptKeyId, GhostConfig, ProviderConfig } from "./config.js";
8
+ import type { GhostConfig } from "./config.js";
9
9
  import type { SnoozeController } from "./snooze.js";
10
- import { type RecentMessage } from "./context-aggregator.js";
11
10
  import type { MetricsLogger } from "./metrics.js";
12
11
  import type { PathIndex } from "./path-index.js";
13
12
  import type { ProfileStore } from "./profile-store.js";
14
13
  export { cycleCandidateIdx, nextLineChunk, nextWordChunk } from "./cycle-manager.js";
15
14
  import { type GhostProvider } from "./providers/types.js";
16
- /**
17
- * #5 helper — classify a stale event as divergent or typed-through given
18
- * the ghost state at render time and the post-change text. Returns true
19
- * when the user typed characters that are NOT a prefix of the suggested
20
- * ghost. Pure / exported for testability — the editor wraps this in
21
- * `recordDivergentTyped` to track the consecutive run.
22
- */
23
- export declare function isTypedPastDivergent(ghost: string, ghostForText: string, textAfterChange: string): boolean;
24
- /** Parse `PI_GHOST_SHORT_HOVER_MS`. Falls back to the default on invalid
25
- * or non-positive input. Exposed for tests; called once per dismiss. */
26
- export declare function parseShortHoverEnv(raw: string | undefined): number;
27
- /** Gap-fix P1.3 — one-shot discoverability suffix on the first painted ghost. */
28
- export declare const ACCEPT_HINT_SUFFIX = " [\u2192 accept]";
29
- /**
30
- * Build the visible ghost string (markers + optional accept hint).
31
- * Pure helper so render logic is unit-testable without a TUI.
32
- */
33
- export declare function formatGhostDisplay(input: {
34
- ghost: string;
35
- replaceMode: boolean;
36
- provisional: boolean;
37
- showAcceptHint: boolean;
38
- }): string;
39
- /**
40
- * Gap-fix P2.2 — which keys trigger a full accept (before popup/cursor guards).
41
- * Pure so unit tests cover the key matrix without a live editor.
42
- */
43
- export declare function isFullAcceptKey(data: string, acceptKeys: readonly AcceptKeyId[]): boolean;
15
+ import { type GhostDiagnostics } from "./ghost-utils.js";
44
16
  /** Gap-fix P2.3 — optional lifecycle events for future AT / telemetry bridges. */
45
17
  export type GhostLifecycleEvent = {
46
18
  type: "shown";
@@ -93,7 +65,15 @@ export declare class GhostEditor extends CustomEditor {
93
65
  private shownMetric;
94
66
  private recentPaths;
95
67
  private recentPathsAt;
68
+ /** pi-api #2 — exact tool-input paths (read/edit/write) with record
69
+ * timestamps. Kept separately from `recentPaths` so the 5 s scrape
70
+ * refresh never drops them; pruned by TOOL_PATH_TTL_MS at merge. */
71
+ private readonly toolPaths;
96
72
  private readonly fastPath;
73
+ /** Static context-selection env knobs (PRD §M6d), snapshotted once at
74
+ * construction instead of re-read per prediction (#3 — was 5 env reads
75
+ * per request in `buildSelectedContext`). */
76
+ private readonly contextEnvOpts;
97
77
  private readonly providerFingerprint;
98
78
  private readonly versionFingerprint;
99
79
  /** Tracks the prefix shown alongside the current ghost so commit-time
@@ -337,6 +317,14 @@ export declare class GhostEditor extends CustomEditor {
337
317
  private trySlashFastPath;
338
318
  private tryDeterministicWithLines;
339
319
  private getRecentPaths;
320
+ /**
321
+ * pi-api-suggestion-sources #2 — record an exact path from a successful
322
+ * read/edit/write tool call. Tool inputs are the precise strings the
323
+ * agent used; regex-scraped message mentions only approximate them.
324
+ * The path is normalized to the PathIndex's coordinate system
325
+ * (root-relative POSIX) so it exact-hits index entries in `recentBoost`.
326
+ */
327
+ recordRecentToolPath(rawPath: string): void;
340
328
  private applyDeterministic;
341
329
  private replaceTokenAtCursor;
342
330
  /**
@@ -359,49 +347,39 @@ export declare class GhostEditor extends CustomEditor {
359
347
  /**
360
348
  * Post-provider-call disposition: stamp TTFT, run the post-flight gate,
361
349
  * sanitize empty results, rerank, apply confidence gate, reconcile with
362
- * any displayed provisional, and commit. Each early-return emits the
363
- * appropriate metric; the happy path lands in `commitDisplayedGhost`.
364
- *
365
- * Extracted from `requestPrediction` as part of refactor #57 phase 1 so
366
- * the orchestrator stays a thin sequence (build → await → finalize).
350
+ * any displayed provisional, and commit. Delegates to
351
+ * `ghost-provider-finalizer.ts` (#57 slice 4d + 4e): the finalization
352
+ * pipeline (finalize + commit) moved out; the editor supplies every
353
+ * collaborator (state refs + bound side-effect callbacks) so mutations
354
+ * stay owned here while the logic is unit-testable in isolation.
367
355
  */
368
356
  private finalizeProviderResult;
369
357
  /**
370
358
  * Centralized post-provider-call disposition. Returns `null` when the
371
359
  * result is good to render; otherwise the abort reason the caller should
372
- * report via `emitMetric`. Clears `this.inflight` only on the happy path
373
- * (matches the prior in-line behavior at the first non-aborted check) so
374
- * an `aborted` return leaves `inflight` for the catch / next request to
375
- * deal with.
360
+ * report via `emitMetric`. Delegates to `ghost-request-pipeline.ts`
361
+ * (#57 slice 4c): the pure fn reads a state snapshot; the editor owns
362
+ * the `inflight` mutation and clears it whenever the fn says to (every
363
+ * branch except the first `aborted`, which leaves `inflight` for the
364
+ * catch / next request to clear).
376
365
  */
377
366
  private postFlightCheck;
378
367
  /**
379
- * PRD §M3.5a — invoke the provider in either streaming or multi-candidate
380
- * mode and (when streaming) drive incremental UI paint via `onChunk`.
381
- * Stamps TTFT on the first stream chunk and returns it for the caller to
382
- * attach to the pending metric.
383
- *
384
- * - Streaming path: provider exposes `predictStream` AND lacks
385
- * `predictMulti`. Used by Mercury FIM streaming + LocalProvider.
386
- * - Multi path: everything else (openai-compat FIM, race with sync
387
- * members). `onChunk` is ignored.
388
- *
389
- * Extracted from `requestPrediction` as part of refactor #57 so the
390
- * orchestrator stays focused on rerank / reconcile / commit.
368
+ * PRD §M3.5a — invoke the provider in streaming or multi-candidate mode
369
+ * and drive incremental UI paint via `onChunk`. Delegates to
370
+ * `ghost-request-pipeline.ts` (#57 slice 4c): pure async dispatch + TTFT
371
+ * stamp, no editor state.
391
372
  */
392
373
  private runPredictionPath;
374
+ private emitMetric;
393
375
  /**
394
- * Commit a successfully-reconciled LLM ghost to all on-screen + fast-path
395
- * structures: ranked candidate list, ghost text, cache, speculative ring,
396
- * and the pending metric record. Extracted from `requestPrediction` as
397
- * part of refactor #57 to keep the orchestrator readable.
398
- *
399
- * Cache + speculative ring store the *reranked* top — a subsequent cache
400
- * hit must reproduce what the user actually saw, not the provider's
401
- * pre-rerank order.
376
+ * Finalize a shown ghost on accept / dismiss / stale / expired: build the
377
+ * metrics record, apply cache/trie side effects, manage the regret-window
378
+ * supersede invariant, and either arm the regret window (accepts) or log.
379
+ * Delegates to `ghost-provider-finalizer.ts` (#57 slice 4g); the regret
380
+ * state machine (handle + timer) stays editor-owned and is surfaced as
381
+ * callbacks so the acceptance decision tree is unit-testable in isolation.
402
382
  */
403
- private commitDisplayedGhost;
404
- private emitMetric;
405
383
  private finalizeShown;
406
384
  /**
407
385
  * Detach + clear the pending-regret watch (handle + timer) atomically
@@ -421,102 +399,3 @@ export declare class GhostEditor extends CustomEditor {
421
399
  */
422
400
  private flushRegret;
423
401
  }
424
- /**
425
- * Render a recent-messages array into a single string for eval capture
426
- * (PRD §M5c). Format mirrors the provider context shape — `role: text`
427
- * joined by newlines — so a CLI replay can feed it back to the same
428
- * provider. Tool entries get a `tool ${name}:` label.
429
- */
430
- export declare function renderContextForEval(recent: RecentMessage[]): string;
431
- export interface GhostDiagnostics {
432
- provider: string | null;
433
- providerFingerprint: string;
434
- versionFingerprint: string;
435
- /** PRD §M3g — capability flags detected from the active provider. */
436
- capabilities: {
437
- streaming: boolean;
438
- multiCandidate: boolean;
439
- preflight: boolean;
440
- /** PRD §M7 — provider populates per-token `-logprob` on stream events. */
441
- logprobs: boolean;
442
- };
443
- cache: {
444
- size: number;
445
- hits: number;
446
- misses: number;
447
- hitRate: number;
448
- };
449
- trie: {
450
- prefixes: number;
451
- candidates: number;
452
- insertsSinceDecay: number;
453
- };
454
- profile: {
455
- enabled: boolean;
456
- ngrams: number;
457
- paths: number;
458
- slash: number;
459
- accepts: number;
460
- decayDays: number;
461
- };
462
- race: {
463
- cooldown: {
464
- active: boolean;
465
- remainingMs: number;
466
- };
467
- members: Array<{
468
- name: string;
469
- slowCount: number;
470
- wins: number;
471
- skipRemaining: number;
472
- }>;
473
- } | null;
474
- ghostShown: boolean;
475
- /** PRD §M5a — regret window state. `null` when no accept is being
476
- * watched; otherwise the remaining ms until the window closes. */
477
- pendingRegret: {
478
- remainingMs: number;
479
- insertedLen: number;
480
- } | null;
481
- /**
482
- * Enhancement #1 — edit-history snapshot for `/ghost` diag. `enabled`
483
- * mirrors `PI_GHOST_EDIT_HISTORY`; the remaining fields are populated
484
- * even when disabled (they'll just report zeros).
485
- */
486
- editHistory: {
487
- enabled: boolean;
488
- events: number;
489
- capacity: number;
490
- uris: number;
491
- topAdded: string[];
492
- topRemoved: string[];
493
- oldestAgeMs: number | null;
494
- halfLifeMs: number;
495
- };
496
- }
497
- /**
498
- * Build a stable provider fingerprint from the config. For race configs, fold
499
- * each member into the `extras` map so member rotation invalidates cache.
500
- */
501
- export declare function providerFingerprintFromConfig(cfg: ProviderConfig): string;
502
- /**
503
- * Pure helper: replace the slice `[from.col, end-of-line]` on `from.line` of
504
- * `text` with `replacement`. Exported for testability.
505
- *
506
- * Caller must guarantee the cursor sits at end of `from.line` and that the
507
- * replacement is intended to land there; this helper does not validate.
508
- */
509
- export declare function computeReplacement(text: string, from: {
510
- line: number;
511
- col: number;
512
- }, replacement: string): string;
513
- /**
514
- * Visible column width of a possibly-ANSI-colored string.
515
- * Uses string-width which handles wide chars, combining marks, and emoji.
516
- */
517
- export declare function visualWidth(s: string): number;
518
- /**
519
- * Clip a string to fit in `maxCols` terminal columns, grapheme-aware.
520
- * Returns the empty string if `maxCols <= 0` or no characters fit.
521
- */
522
- export declare function clipToWidth(s: string, maxCols: number): string;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * SPDX-License-Identifier: SSPL-1.0
3
+ * SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro.pungitore.ch>
4
+ */
5
+ import type { AcceptKeyId, ProviderConfig } from "./config.js";
6
+ import type { RecentMessage } from "./context-aggregator.js";
7
+ /** Parse `PI_GHOST_SHORT_HOVER_MS`. Falls back to the default on invalid
8
+ * or non-positive input. Exposed for tests; called once per dismiss. */
9
+ export declare function parseShortHoverEnv(raw: string | undefined): number;
10
+ /**
11
+ * #5 — was the user's typed tail divergent from the ghost? `false` when the
12
+ * ghost is empty, when nothing was typed, or when the typed tail is a prefix
13
+ * of the ghost (accept-in-progress). Drives `recordDivergentTyped`.
14
+ */
15
+ export declare function isTypedPastDivergent(ghost: string, ghostForText: string, textAfterChange: string): boolean;
16
+ /** Gap-fix P1.3 — one-shot discoverability suffix on the first painted ghost. */
17
+ export declare const ACCEPT_HINT_SUFFIX = " [\u2192 accept]";
18
+ /** Historical ghost styling (pi < 0.99): SGR 2 faint + bright black (256-color 8/90). */
19
+ export declare const GHOST_ANSI_FALLBACK: Readonly<{
20
+ open: string;
21
+ close: string;
22
+ }>;
23
+ /**
24
+ * Theme-aware ghost text styling. Pi 0.99 exposes the live `Theme` object as
25
+ * `ctx.ui.theme`; its `getFgAnsi("dim")` returns the opening escape sequence
26
+ * for the active theme's dim/faint token (faint tokens include SGR 2). With
27
+ * 0.99's default `system` theme the color is derived from the terminal's own
28
+ * palette and follows light/dark switches — instead of our hardcoded
29
+ * bright-black, which can be invisible or indistinguishable on palettes where
30
+ * color 8 sits near the foreground or background. Feature-detected so pi
31
+ * 0.84 hosts keep the historical fallback; pure over the passed ui handle.
32
+ */
33
+ export declare function resolveGhostAnsi(ui: unknown): {
34
+ open: string;
35
+ close: string;
36
+ };
37
+ /**
38
+ * Build the visible ghost string (markers + optional accept hint).
39
+ * Pure helper so render logic is unit-testable without a TUI.
40
+ */
41
+ export declare function formatGhostDisplay(input: {
42
+ ghost: string;
43
+ replaceMode: boolean;
44
+ provisional: boolean;
45
+ showAcceptHint: boolean;
46
+ }): string;
47
+ /**
48
+ * Gap-fix P2.2 — which keys trigger a full accept (before popup/cursor guards).
49
+ * Pure so unit tests cover the key matrix without a live editor.
50
+ */
51
+ export declare function isFullAcceptKey(data: string, acceptKeys: readonly AcceptKeyId[]): boolean;
52
+ /**
53
+ * Build a stable provider fingerprint from the config. For race configs, fold
54
+ * each member into the `extras` map so member rotation invalidates cache.
55
+ */
56
+ export declare function providerFingerprintFromConfig(cfg: ProviderConfig): string;
57
+ /**
58
+ * Render a recent-messages array into a single string for eval capture
59
+ * (PRD §M5c). Format mirrors the provider context shape — `role: text`
60
+ * joined by newlines — so a CLI replay can feed it back to the same
61
+ * provider. Tool entries get a `tool ${name}:` label.
62
+ */
63
+ export declare function renderContextForEval(recent: RecentMessage[]): string;
64
+ /**
65
+ * Pure helper: replace the slice `[from.col, end-of-line]` on `from.line` of
66
+ * `text` with `replacement`. Exported for testability.
67
+ *
68
+ * Caller must guarantee the cursor sits at end of `from.line` and that the
69
+ * replacement is intended to land there; this helper does not validate.
70
+ */
71
+ export declare function computeReplacement(text: string, from: {
72
+ line: number;
73
+ col: number;
74
+ }, replacement: string): string;
75
+ /**
76
+ * Visible column width of a possibly-ANSI-colored string.
77
+ * Uses string-width which handles wide chars, combining marks, and emoji.
78
+ */
79
+ export declare function visualWidth(s: string): number;
80
+ /**
81
+ * Clip a string to fit in `maxCols` terminal columns, grapheme-aware.
82
+ * Returns the empty string if `maxCols <= 0` or no characters fit.
83
+ */
84
+ export declare function clipToWidth(s: string, maxCols: number): string;
85
+ export interface GhostDiagnostics {
86
+ provider: string | null;
87
+ providerFingerprint: string;
88
+ versionFingerprint: string;
89
+ /** PRD §M3g — capability flags detected from the active provider. */
90
+ capabilities: {
91
+ streaming: boolean;
92
+ multiCandidate: boolean;
93
+ preflight: boolean;
94
+ /** PRD §M7 — provider populates per-token `-logprob` on stream events. */
95
+ logprobs: boolean;
96
+ };
97
+ cache: {
98
+ size: number;
99
+ hits: number;
100
+ misses: number;
101
+ hitRate: number;
102
+ };
103
+ trie: {
104
+ prefixes: number;
105
+ candidates: number;
106
+ insertsSinceDecay: number;
107
+ };
108
+ profile: {
109
+ enabled: boolean;
110
+ ngrams: number;
111
+ paths: number;
112
+ slash: number;
113
+ accepts: number;
114
+ decayDays: number;
115
+ };
116
+ race: {
117
+ cooldown: {
118
+ active: boolean;
119
+ remainingMs: number;
120
+ };
121
+ members: Array<{
122
+ name: string;
123
+ slowCount: number;
124
+ wins: number;
125
+ skipRemaining: number;
126
+ }>;
127
+ } | null;
128
+ ghostShown: boolean;
129
+ /** PRD §M5a — regret window state. `null` when no accept is being
130
+ * watched; otherwise the remaining ms until the window closes. */
131
+ pendingRegret: {
132
+ remainingMs: number;
133
+ insertedLen: number;
134
+ } | null;
135
+ /**
136
+ * Enhancement #1 — edit-history snapshot for `/ghost` diag. `enabled`
137
+ * mirrors `PI_GHOST_EDIT_HISTORY`; the remaining fields are populated
138
+ * even when disabled (they'll just report zeros).
139
+ */
140
+ editHistory: {
141
+ enabled: boolean;
142
+ events: number;
143
+ capacity: number;
144
+ uris: number;
145
+ topAdded: string[];
146
+ topRemoved: string[];
147
+ oldestAgeMs: number | null;
148
+ halfLifeMs: number;
149
+ };
150
+ }
package/dist/index.d.ts CHANGED
@@ -2,10 +2,20 @@
2
2
  * SPDX-License-Identifier: SSPL-1.0
3
3
  * SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
4
4
  */
5
- import { type ExtensionFactory } from "@earendil-works/pi-coding-agent";
6
- import { type GhostConfig } from "./config.js";
7
- import type { GhostDiagnostics } from "./ghost-editor.js";
5
+ import { type ExtensionFactory, type ModelRegistry } from "@earendil-works/pi-coding-agent";
6
+ import { type GhostConfig, type PiModelProviderConfig } from "./config.js";
7
+ import type { GhostDiagnostics } from "./ghost-utils.js";
8
+ import { PiModelProvider } from "./providers/pi-model-provider.js";
8
9
  declare const ghost: ExtensionFactory;
10
+ /**
11
+ * Build a router's pi chat leg and drop it when its (local-only, cheap)
12
+ * preflight fails. Returned warning is null when the leg is healthy.
13
+ * Exported for tests; pure over (config, registry).
14
+ */
15
+ export declare function resolvePiChatLeg(cfg: PiModelProviderConfig, registry: ModelRegistry): Promise<{
16
+ provider: PiModelProvider | null;
17
+ warning: string | null;
18
+ }>;
9
19
  /**
10
20
  * PRD §M3g — render the diagnostic snapshot from `GhostEditor.getDiagnostics`
11
21
  * alongside the provider config. The output is a single notify-friendly
@@ -32,8 +42,10 @@ export { RouterProvider, routeToChat, routeToChatByRegister } from "./providers/
32
42
  export type { PreflightResult } from "./providers/openai-compat-provider.js";
33
43
  export { RaceProvider, firstNonNull } from "./providers/race.js";
34
44
  export type { RaceConfig, RaceTrace } from "./providers/race.js";
35
- export { GhostEditor, computeReplacement, isFullAcceptKey } from "./ghost-editor.js";
36
- export type { GhostDiagnostics, GhostLifecycleEvent, GhostLifecycleListener } from "./ghost-editor.js";
45
+ export { GhostEditor } from "./ghost-editor.js";
46
+ export type { GhostLifecycleEvent, GhostLifecycleListener } from "./ghost-editor.js";
47
+ export { computeReplacement, isFullAcceptKey } from "./ghost-utils.js";
48
+ export type { GhostDiagnostics } from "./ghost-utils.js";
37
49
  export type { GhostConfig, AcceptKeyId, ProviderConfig, CloudProviderConfig, LocalProviderConfig, OpenAICompatProviderConfig, OpenAICompatChatProviderConfig, RaceProviderConfig, RouterProviderConfig, } from "./config.js";
38
50
  export { parseAcceptKeys, DEFAULT_ACCEPT_KEYS } from "./config.js";
39
51
  export { BUILTIN_SLASH_COMMANDS, mergeKnownCommands, seedCommandTrie, COLD_START_TRIE_WEIGHT, } from "./cold-start.js";