@jmcombs/pi-context7 1.0.0 → 2.0.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/README.md +40 -18
- package/index.ts +61 -247
- package/package.json +4 -3
- package/ui/bordered-popups.ts +0 -319
package/README.md
CHANGED
|
@@ -13,6 +13,33 @@
|
|
|
13
13
|
|
|
14
14
|
Real-time documentation for the Pi coding agent via [Context7](https://context7.com). Gives the agent access to up-to-date, version-aware docs and code examples without polluting context with outdated information.
|
|
15
15
|
|
|
16
|
+
## Breaking changes in v2.0.0
|
|
17
|
+
|
|
18
|
+
- **`AuthStorage` is gone.** Pi 0.80.8 removed the `AuthStorage` API this extension used to store and read its API key. Credentials now resolve through the [`@jmcombs/pi-1password`](https://www.npmjs.com/package/@jmcombs/pi-1password) **credential API**, which this package now depends on directly and **installs automatically** — no separate install step.
|
|
19
|
+
- **Availability-branched onboarding.** When the `op` CLI is installed and an account is configured, setup opens a 1Password **vault → item → field picker**; when `op` is unavailable it falls back to **masked manual key entry**.
|
|
20
|
+
- **Existing keys keep working.** Any Context7 key already in `~/.pi/agent/auth.json` — a literal value or an `!op read` reference — resolves unchanged. No migration action is required.
|
|
21
|
+
|
|
22
|
+
## What's New — 1Password credential integration
|
|
23
|
+
|
|
24
|
+
Context7 now handles your API key through the [`@jmcombs/pi-1password`](https://www.npmjs.com/package/@jmcombs/pi-1password) credential API, which installs automatically as a dependency. What this means for you:
|
|
25
|
+
|
|
26
|
+
- **Onboarding branches on 1Password availability.** If the `op` CLI is installed and an account is configured, `/context7_setup` opens a live **vault → item → field picker** (or lets you type an `op://…` reference) and stores it as a `!op read '…'` entry that resolves fresh on every use. If `op` is not available, it falls back to **manual API-key entry** and nudges you to enable the 1Password extension for vault integration.
|
|
27
|
+
- **Existing keys keep working.** Any Context7 key already in `~/.pi/agent/auth.json` — a literal key or an `!op read` reference — continues to resolve unchanged. No migration action is required.
|
|
28
|
+
- **The key is never exposed to the model.** Entry happens entirely in the TUI, and only the resolved value is used to call the Context7 API.
|
|
29
|
+
- **Startup warm-up.** With the [`@jmcombs/pi-1password`](https://www.npmjs.com/package/@jmcombs/pi-1password) extension installed and enabled, a one-time `op read` runs at session startup, so the 1Password biometric unlock prompt lands once and later key resolves are silent.
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart TD
|
|
33
|
+
A["/context7_setup or first tool use"] --> B{"is1PasswordAvailable()<br/>(op installed AND configured)"}
|
|
34
|
+
B -- "Yes" --> C["Live vault → item → field picker<br/>or manual op:// reference"]
|
|
35
|
+
C --> D["Store !op read 'op://…' entry"]
|
|
36
|
+
B -- "No" --> E["Manual API-key entry<br/>+ nudge to enable 1Password"]
|
|
37
|
+
E --> F["Store literal api_key entry"]
|
|
38
|
+
D --> G["resolveSecret('context7')<br/>resolves fresh on each tool call"]
|
|
39
|
+
F --> G
|
|
40
|
+
G --> H["Bearer token → Context7 API<br/>(never shown to the LLM)"]
|
|
41
|
+
```
|
|
42
|
+
|
|
16
43
|
## Quick Start
|
|
17
44
|
|
|
18
45
|
Get better library documentation in your agent in under a minute.
|
|
@@ -20,16 +47,16 @@ Get better library documentation in your agent in under a minute.
|
|
|
20
47
|
1. Install the extension:
|
|
21
48
|
|
|
22
49
|
```bash
|
|
23
|
-
pi install
|
|
50
|
+
pi install npm:@jmcombs/pi-context7
|
|
24
51
|
```
|
|
25
52
|
|
|
26
53
|
2. Configure your Context7 API key:
|
|
27
54
|
|
|
28
55
|
```
|
|
29
|
-
/
|
|
56
|
+
/context7_setup
|
|
30
57
|
```
|
|
31
58
|
|
|
32
|
-
The command opens
|
|
59
|
+
The command opens the onboarding flow above — a 1Password vault picker when `op` is available, or a secure manual-entry prompt otherwise. The value is never visible to the LLM.
|
|
33
60
|
|
|
34
61
|
After setup, just ask the agent for documentation normally:
|
|
35
62
|
|
|
@@ -46,28 +73,23 @@ This extension registers two tools:
|
|
|
46
73
|
- `context7_search` — Finds the correct Context7 library ID for a programming language, framework, or library.
|
|
47
74
|
- `context7_get_docs` — Retrieves detailed, version-specific documentation and real code examples for that library.
|
|
48
75
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- **Persisted keys** — Saved via `/context7_onboard` into `~/.pi/agent/auth.json` (supports plain keys and `!op read` references).
|
|
52
|
-
- **Runtime-only keys** — Entered ad-hoc when a tool is called and kept only for the current session (also supports `!op read` references).
|
|
53
|
-
|
|
54
|
-
This design lets you use Context7 without ever leaking keys into the LLM context.
|
|
76
|
+
Both tools resolve the key through `resolveSecret("context7")` from `@jmcombs/pi-1password`, reading `~/.pi/agent/auth.json` fresh on each call (a literal key or an `!op read` reference). If no key is stored, the tool automatically runs onboarding (the availability branch above), then re-resolves — preserving the "prompt on first use" experience. The key is never leaked into the LLM context.
|
|
55
77
|
|
|
56
|
-
## /
|
|
78
|
+
## /context7_setup
|
|
57
79
|
|
|
58
|
-
Run this command to
|
|
80
|
+
Run this command to configure (or update) your Context7 API key at any time:
|
|
59
81
|
|
|
60
82
|
```
|
|
61
|
-
/
|
|
83
|
+
/context7_setup
|
|
62
84
|
```
|
|
63
85
|
|
|
64
|
-
It
|
|
86
|
+
It delegates to the `@jmcombs/pi-1password` onboarding flow, which:
|
|
65
87
|
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
88
|
+
- Branches on 1Password availability — vault picker (`op://` reference) when `op` is configured, secure manual entry otherwise.
|
|
89
|
+
- Stores the value in `~/.pi/agent/auth.json` (`0600`), never exposing it to the model.
|
|
90
|
+
- Reports the outcome as a status notification.
|
|
69
91
|
|
|
70
|
-
|
|
92
|
+
To rotate an existing key, install and enable the [`@jmcombs/pi-1password`](https://www.npmjs.com/package/@jmcombs/pi-1password) extension and re-run onboarding through it, or edit `auth.json` directly.
|
|
71
93
|
|
|
72
94
|
## After Setup
|
|
73
95
|
|
|
@@ -81,7 +103,7 @@ Examples of good prompts:
|
|
|
81
103
|
|
|
82
104
|
## Checking Status
|
|
83
105
|
|
|
84
|
-
|
|
106
|
+
`/context7_setup` will not silently overwrite an existing key — if one is already stored it reports that and leaves it in place. To rotate a key, remove the existing `context7` entry from `~/.pi/agent/auth.json` (or update it through the `@jmcombs/pi-1password` extension), then run `/context7_setup` again.
|
|
85
107
|
|
|
86
108
|
## Development
|
|
87
109
|
|
package/index.ts
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers `context7_search` and `context7_get_docs` tools that let the LLM
|
|
5
5
|
* find and retrieve version-aware documentation and code snippets from the
|
|
6
|
-
* Context7 API.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Context7 API. Credentials are handled entirely through the imported
|
|
7
|
+
* `@jmcombs/pi-1password` credential API (`resolveSecret` / `onboardSecret`),
|
|
8
|
+
* so the key is never leaked into the agent's context.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* 1. `
|
|
12
|
-
*
|
|
10
|
+
* Credential handling:
|
|
11
|
+
* 1. `resolveSecret("context7")` reads `~/.pi/agent/auth.json` and resolves the
|
|
12
|
+
* stored entry (literal key or `!op read 'op://…'` reference) fresh on each use.
|
|
13
|
+
* 2. If nothing is stored, the tool auto-invokes `onboardSecret`, which branches
|
|
14
|
+
* on 1Password availability — the live vault picker when `op` is configured,
|
|
15
|
+
* manual API-key entry otherwise — then re-resolves.
|
|
16
|
+
* 3. `/context7_setup` runs the same onboarding flow on demand.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
19
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { onboardSecret, resolveSecret } from "@jmcombs/pi-1password";
|
|
21
|
+
import { type Static, Type } from "typebox";
|
|
18
22
|
|
|
19
23
|
const CONTEXT7_API_BASE = "https://context7.com/api/v2";
|
|
20
24
|
|
|
@@ -75,7 +79,7 @@ function formatDocs(data: Context7DocsResponse, query: string): string {
|
|
|
75
79
|
const { codeSnippets = [], infoSnippets = [] } = data;
|
|
76
80
|
|
|
77
81
|
if (codeSnippets.length === 0 && infoSnippets.length === 0) {
|
|
78
|
-
return
|
|
82
|
+
return `No documentation snippets found for ${query}.`;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
const parts: string[] = [];
|
|
@@ -84,13 +88,13 @@ function formatDocs(data: Context7DocsResponse, query: string): string {
|
|
|
84
88
|
parts.push("--- CODE SNIPPETS ---");
|
|
85
89
|
for (const snippet of codeSnippets) {
|
|
86
90
|
if (snippet.codeTitle) {
|
|
87
|
-
parts.push(
|
|
91
|
+
parts.push(`\n## ${snippet.codeTitle}`);
|
|
88
92
|
}
|
|
89
93
|
if (snippet.codeList && snippet.codeList.length > 0) {
|
|
90
94
|
for (const item of snippet.codeList) {
|
|
91
95
|
if (item.code) {
|
|
92
96
|
const lang = item.language ?? "typescript";
|
|
93
|
-
parts.push(
|
|
97
|
+
parts.push(`\`\`\`${lang}\n${item.code}\n\`\`\`\n`);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
}
|
|
@@ -101,57 +105,23 @@ function formatDocs(data: Context7DocsResponse, query: string): string {
|
|
|
101
105
|
parts.push("\n--- INFO SNIPPETS ---");
|
|
102
106
|
for (const snippet of infoSnippets) {
|
|
103
107
|
if (snippet.content) {
|
|
104
|
-
parts.push(
|
|
108
|
+
parts.push(`\n${snippet.content}`);
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
return
|
|
113
|
+
return `Documentation for ${query}:\n\n${parts.join("\n")}`;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
// -- Extension factory
|
|
113
117
|
|
|
114
118
|
export default function (pi: ExtensionAPI): void {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
description: "Securely save your Context7 API key (input never visible to LLM).",
|
|
119
|
+
// -- /context7_setup (user-facing command)
|
|
120
|
+
pi.registerCommand("context7_setup", {
|
|
121
|
+
description: "Set up or update your Context7 API key (never shown to the agent).",
|
|
119
122
|
handler: async (_args, ctx) => {
|
|
120
|
-
ctx
|
|
121
|
-
|
|
122
|
-
const existing = await authStorage.getApiKey("context7");
|
|
123
|
-
if (existing) {
|
|
124
|
-
const overwrite = await confirmInBorderedPopup(ctx, {
|
|
125
|
-
title: "Context7 API key already exists, overwrite?",
|
|
126
|
-
message: "A Context7 API key is already saved. Overwrite it?",
|
|
127
|
-
});
|
|
128
|
-
if (!overwrite) {
|
|
129
|
-
ctx.ui.notify("Context7 onboarding cancelled.", "warning");
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const apiKey = await inputInBorderedPopup(ctx, {
|
|
135
|
-
title: "Context7 Onboarding",
|
|
136
|
-
prompt: "Enter your Context7 API key:",
|
|
137
|
-
helpText: "Enter to confirm • Esc = cancel",
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!apiKey) {
|
|
141
|
-
ctx.ui.notify("Context7 onboarding cancelled.", "warning");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
authStorage.set("context7", {
|
|
146
|
-
type: "api_key" as const,
|
|
147
|
-
key: apiKey,
|
|
148
|
-
});
|
|
149
|
-
authStorage.removeRuntimeApiKey("context7");
|
|
150
|
-
const errs = authStorage.drainErrors();
|
|
151
|
-
if (errs.length > 0) {
|
|
152
|
-
ctx.ui.notify(`onboard ERRORS: ${errs.map((e) => e.message).join("; ")}`, "warning");
|
|
153
|
-
}
|
|
154
|
-
ctx.ui.notify("Context7 API key saved successfully.", "info");
|
|
123
|
+
const result = await onboardSecret(ctx, { name: "context7", label: "Context7" });
|
|
124
|
+
ctx.ui.notify(result.message, result.ok ? "info" : "warning");
|
|
155
125
|
},
|
|
156
126
|
});
|
|
157
127
|
|
|
@@ -165,97 +135,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
165
135
|
"Always prefer this tool over general web search when you need accurate, version-aware information for coding or development tasks.",
|
|
166
136
|
parameters: context7SearchSchema,
|
|
167
137
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
168
|
-
let apiKey = await
|
|
169
|
-
|
|
138
|
+
let apiKey = await resolveSecret("context7");
|
|
170
139
|
if (!apiKey) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
helpText: "Enter to confirm • Esc = cancel",
|
|
175
|
-
});
|
|
176
|
-
if (!entered) {
|
|
177
|
-
ctx.ui.notify("Context7 search cancelled.", "warning");
|
|
178
|
-
return {
|
|
179
|
-
content: [
|
|
180
|
-
{
|
|
181
|
-
type: "text",
|
|
182
|
-
text: "Search cancelled: no Context7 API key provided.",
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
details: { error: "missing_api_key" },
|
|
186
|
-
isError: true,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Always use the entered value for this request (supports raw keys and !op read)
|
|
191
|
-
apiKey = entered;
|
|
192
|
-
|
|
193
|
-
const savePermanently = await confirmInBorderedPopup(ctx, {
|
|
194
|
-
title: "Save API Key?",
|
|
195
|
-
message:
|
|
196
|
-
"Save this value permanently in auth.json?\n\n" +
|
|
197
|
-
"Note: 1Password references (!op read ...) only resolve when saved permanently. " +
|
|
198
|
-
"Choosing No will store the resolved secret for this session only.",
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (savePermanently === null) {
|
|
202
|
-
// User cancelled the confirmation dialog — abort the entire key entry
|
|
203
|
-
ctx.ui.notify("Context7 authentication cancelled.", "warning");
|
|
204
|
-
return {
|
|
205
|
-
content: [
|
|
206
|
-
{
|
|
207
|
-
type: "text",
|
|
208
|
-
text:
|
|
209
|
-
"The user explicitly cancelled Context7 authentication. " +
|
|
210
|
-
"Do not attempt to use context7_search or context7_get_docs again in this session " +
|
|
211
|
-
"without the user re-initiating the flow.",
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
details: { error: "cancelled" },
|
|
215
|
-
isError: true,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (savePermanently) {
|
|
220
|
-
authStorage.set("context7", {
|
|
221
|
-
type: "api_key" as const,
|
|
222
|
-
key: entered,
|
|
223
|
-
});
|
|
224
|
-
authStorage.removeRuntimeApiKey("context7");
|
|
225
|
-
const errs = authStorage.drainErrors();
|
|
226
|
-
if (errs.length > 0) {
|
|
227
|
-
ctx.ui.notify(
|
|
228
|
-
`[context7] search save ERRORS: ${errs.map((e) => e.message).join("; ")}`,
|
|
229
|
-
"warning",
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
let keyForRuntime = entered;
|
|
234
|
-
if (entered.trim().startsWith("!op read")) {
|
|
235
|
-
// Temporarily store the reference so AuthStorage resolves it via the normal path (1Password, etc.)
|
|
236
|
-
authStorage.set("context7", { type: "api_key" as const, key: entered });
|
|
237
|
-
const resolved = await authStorage.getApiKey("context7");
|
|
238
|
-
authStorage.remove("context7");
|
|
239
|
-
if (resolved) {
|
|
240
|
-
keyForRuntime = resolved;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
authStorage.setRuntimeApiKey("context7", keyForRuntime);
|
|
244
|
-
}
|
|
245
|
-
apiKey = (await authStorage.getApiKey("context7")) ?? entered;
|
|
246
|
-
if (!apiKey) {
|
|
247
|
-
return {
|
|
248
|
-
content: [
|
|
249
|
-
{
|
|
250
|
-
type: "text",
|
|
251
|
-
text: "Failed to resolve Context7 API key. Check your shell configuration.",
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
details: { error: "missing_api_key" },
|
|
255
|
-
isError: true,
|
|
256
|
-
};
|
|
140
|
+
const r = await onboardSecret(ctx, { name: "context7", label: "Context7" });
|
|
141
|
+
if (r.ok) {
|
|
142
|
+
apiKey = await resolveSecret("context7");
|
|
257
143
|
}
|
|
258
144
|
}
|
|
145
|
+
if (!apiKey) {
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: "Search cancelled: no Context7 API key provided.",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
details: { error: "missing_api_key" },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
259
156
|
|
|
260
157
|
try {
|
|
261
158
|
const url = new URL("/api/v2/libs/search", CONTEXT7_API_BASE);
|
|
@@ -266,7 +163,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
266
163
|
|
|
267
164
|
const response = await fetch(url.toString(), {
|
|
268
165
|
signal,
|
|
269
|
-
headers: { Authorization:
|
|
166
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
270
167
|
});
|
|
271
168
|
|
|
272
169
|
if (!response.ok) {
|
|
@@ -277,11 +174,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
277
174
|
type: "text",
|
|
278
175
|
text:
|
|
279
176
|
"Context7 API error: 401 Unauthorized. Your Context7 API key " +
|
|
280
|
-
"may be missing or invalid. Run /
|
|
177
|
+
"may be missing or invalid. Run /context7_setup to configure it.",
|
|
281
178
|
},
|
|
282
179
|
],
|
|
283
180
|
details: { status: 401 },
|
|
284
|
-
isError: true,
|
|
285
181
|
};
|
|
286
182
|
}
|
|
287
183
|
if (response.status === 429) {
|
|
@@ -295,7 +191,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
295
191
|
},
|
|
296
192
|
],
|
|
297
193
|
details: { status: 429 },
|
|
298
|
-
isError: true,
|
|
299
194
|
};
|
|
300
195
|
}
|
|
301
196
|
|
|
@@ -314,7 +209,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
314
209
|
},
|
|
315
210
|
],
|
|
316
211
|
details: { status: response.status, body: errorText },
|
|
317
|
-
isError: true,
|
|
318
212
|
};
|
|
319
213
|
}
|
|
320
214
|
|
|
@@ -326,7 +220,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
326
220
|
content: [
|
|
327
221
|
{
|
|
328
222
|
type: "text",
|
|
329
|
-
text:
|
|
223
|
+
text: `No libraries found matching ${params.libraryName}.`,
|
|
330
224
|
},
|
|
331
225
|
],
|
|
332
226
|
details: { libraryName: params.libraryName, raw: data },
|
|
@@ -334,9 +228,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
334
228
|
}
|
|
335
229
|
|
|
336
230
|
const formatted = libs
|
|
337
|
-
.map(
|
|
338
|
-
return String(i + 1) + ". " + lib.title + " (ID: " + lib.id + ")";
|
|
339
|
-
})
|
|
231
|
+
.map((lib, i) => `${String(i + 1)}. ${lib.title} (ID: ${lib.id})`)
|
|
340
232
|
.join("\n");
|
|
341
233
|
|
|
342
234
|
return {
|
|
@@ -359,11 +251,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
359
251
|
content: [
|
|
360
252
|
{
|
|
361
253
|
type: "text",
|
|
362
|
-
text:
|
|
254
|
+
text: `Error performing Context7 search: ${message}`,
|
|
363
255
|
},
|
|
364
256
|
],
|
|
365
257
|
details: { error: message },
|
|
366
|
-
isError: true,
|
|
367
258
|
};
|
|
368
259
|
}
|
|
369
260
|
},
|
|
@@ -379,97 +270,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
379
270
|
"You should usually call context7_search first to obtain the correct Library ID. Prefer this tool when you need reliable, current technical documentation rather than general explanations.",
|
|
380
271
|
parameters: context7GetDocsSchema,
|
|
381
272
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
382
|
-
let apiKey = await
|
|
383
|
-
|
|
273
|
+
let apiKey = await resolveSecret("context7");
|
|
384
274
|
if (!apiKey) {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
helpText: "Enter to confirm • Esc = cancel",
|
|
389
|
-
});
|
|
390
|
-
if (!entered) {
|
|
391
|
-
ctx.ui.notify("Context7 documentation retrieval cancelled.", "warning");
|
|
392
|
-
return {
|
|
393
|
-
content: [
|
|
394
|
-
{
|
|
395
|
-
type: "text",
|
|
396
|
-
text: "Documentation retrieval cancelled: no Context7 API key provided.",
|
|
397
|
-
},
|
|
398
|
-
],
|
|
399
|
-
details: { error: "missing_api_key" },
|
|
400
|
-
isError: true,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Always use the entered value for this request (supports raw keys and !op read)
|
|
405
|
-
apiKey = entered;
|
|
406
|
-
|
|
407
|
-
const savePermanently = await confirmInBorderedPopup(ctx, {
|
|
408
|
-
title: "Save API Key?",
|
|
409
|
-
message:
|
|
410
|
-
"Save this value permanently in auth.json?\n\n" +
|
|
411
|
-
"Note: 1Password references (!op read ...) only resolve when saved permanently. " +
|
|
412
|
-
"Choosing No will store the resolved secret for this session only.",
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
if (savePermanently === null) {
|
|
416
|
-
// User cancelled the confirmation dialog — abort the entire key entry
|
|
417
|
-
ctx.ui.notify("Context7 authentication cancelled.", "warning");
|
|
418
|
-
return {
|
|
419
|
-
content: [
|
|
420
|
-
{
|
|
421
|
-
type: "text",
|
|
422
|
-
text:
|
|
423
|
-
"The user explicitly cancelled Context7 authentication. " +
|
|
424
|
-
"Do not attempt to use context7_search or context7_get_docs again in this session " +
|
|
425
|
-
"without the user re-initiating the flow.",
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
details: { error: "cancelled" },
|
|
429
|
-
isError: true,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (savePermanently) {
|
|
434
|
-
authStorage.set("context7", {
|
|
435
|
-
type: "api_key" as const,
|
|
436
|
-
key: entered,
|
|
437
|
-
});
|
|
438
|
-
authStorage.removeRuntimeApiKey("context7");
|
|
439
|
-
const errs = authStorage.drainErrors();
|
|
440
|
-
if (errs.length > 0) {
|
|
441
|
-
ctx.ui.notify(
|
|
442
|
-
`get_docs save ERRORS: ${errs.map((e) => e.message).join("; ")}`,
|
|
443
|
-
"warning",
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
} else {
|
|
447
|
-
let keyForRuntime = entered;
|
|
448
|
-
if (entered.trim().startsWith("!op read")) {
|
|
449
|
-
// Temporarily store the reference so AuthStorage resolves it via the normal path (1Password, etc.)
|
|
450
|
-
authStorage.set("context7", { type: "api_key" as const, key: entered });
|
|
451
|
-
const resolved = await authStorage.getApiKey("context7");
|
|
452
|
-
authStorage.remove("context7");
|
|
453
|
-
if (resolved) {
|
|
454
|
-
keyForRuntime = resolved;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
authStorage.setRuntimeApiKey("context7", keyForRuntime);
|
|
458
|
-
}
|
|
459
|
-
apiKey = (await authStorage.getApiKey("context7")) ?? entered;
|
|
460
|
-
if (!apiKey) {
|
|
461
|
-
return {
|
|
462
|
-
content: [
|
|
463
|
-
{
|
|
464
|
-
type: "text",
|
|
465
|
-
text: "Failed to resolve Context7 API key. Check your shell configuration.",
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
details: { error: "missing_api_key" },
|
|
469
|
-
isError: true,
|
|
470
|
-
};
|
|
275
|
+
const r = await onboardSecret(ctx, { name: "context7", label: "Context7" });
|
|
276
|
+
if (r.ok) {
|
|
277
|
+
apiKey = await resolveSecret("context7");
|
|
471
278
|
}
|
|
472
279
|
}
|
|
280
|
+
if (!apiKey) {
|
|
281
|
+
return {
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: "Documentation retrieval cancelled: no Context7 API key provided.",
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
details: { error: "missing_api_key" },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
473
291
|
|
|
474
292
|
try {
|
|
475
293
|
const url = new URL("/api/v2/context", CONTEXT7_API_BASE);
|
|
@@ -479,7 +297,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
479
297
|
|
|
480
298
|
const response = await fetch(url.toString(), {
|
|
481
299
|
signal,
|
|
482
|
-
headers: { Authorization:
|
|
300
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
483
301
|
});
|
|
484
302
|
|
|
485
303
|
if (!response.ok) {
|
|
@@ -490,11 +308,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
490
308
|
type: "text",
|
|
491
309
|
text:
|
|
492
310
|
"Context7 API error: 401 Unauthorized. Your Context7 API key " +
|
|
493
|
-
"may be missing or invalid. Run /
|
|
311
|
+
"may be missing or invalid. Run /context7_setup to configure it.",
|
|
494
312
|
},
|
|
495
313
|
],
|
|
496
314
|
details: { status: 401 },
|
|
497
|
-
isError: true,
|
|
498
315
|
};
|
|
499
316
|
}
|
|
500
317
|
if (response.status === 429) {
|
|
@@ -508,7 +325,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
508
325
|
},
|
|
509
326
|
],
|
|
510
327
|
details: { status: 429 },
|
|
511
|
-
isError: true,
|
|
512
328
|
};
|
|
513
329
|
}
|
|
514
330
|
|
|
@@ -527,7 +343,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
527
343
|
},
|
|
528
344
|
],
|
|
529
345
|
details: { status: response.status, body: errorText },
|
|
530
|
-
isError: true,
|
|
531
346
|
};
|
|
532
347
|
}
|
|
533
348
|
|
|
@@ -542,11 +357,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
542
357
|
content: [
|
|
543
358
|
{
|
|
544
359
|
type: "text",
|
|
545
|
-
text:
|
|
360
|
+
text: `Error fetching Context7 documentation: ${message}`,
|
|
546
361
|
},
|
|
547
362
|
],
|
|
548
363
|
details: { error: message },
|
|
549
|
-
isError: true,
|
|
550
364
|
};
|
|
551
365
|
}
|
|
552
366
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmcombs/pi-context7",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Real-time documentation for the Pi coding agent via Context7.",
|
|
6
6
|
"homepage": "https://github.com/jmcombs/pi-extensions/tree/main/packages/context7",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"types": "./index.ts",
|
|
20
20
|
"files": [
|
|
21
21
|
"index.ts",
|
|
22
|
-
"ui/",
|
|
23
22
|
"README.md",
|
|
24
23
|
"LICENSE"
|
|
25
24
|
],
|
|
@@ -37,9 +36,11 @@
|
|
|
37
36
|
"engines": {
|
|
38
37
|
"node": ">=22.0.0"
|
|
39
38
|
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@jmcombs/pi-1password": "^1.0.2 || ^2.0.0"
|
|
41
|
+
},
|
|
40
42
|
"peerDependencies": {
|
|
41
43
|
"@earendil-works/pi-coding-agent": "*",
|
|
42
|
-
"@earendil-works/pi-tui": "*",
|
|
43
44
|
"typebox": "*"
|
|
44
45
|
}
|
|
45
46
|
}
|
package/ui/bordered-popups.ts
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bordered Popup TUI Helpers
|
|
3
|
-
*
|
|
4
|
-
* These are reusable, self-contained helpers for creating polished,
|
|
5
|
-
* consistent bordered popups inside Pi extensions using `ctx.ui.custom({ overlay: true })`.
|
|
6
|
-
*
|
|
7
|
-
* They were originally developed in the 1Password extension and are provided
|
|
8
|
-
* here as a copy-paste starting point for any extension that needs rich
|
|
9
|
-
* interactive flows (select lists with live filtering, text input, confirms)
|
|
10
|
-
* that look and feel better than the basic `ctx.ui.select / input / confirm`.
|
|
11
|
-
*
|
|
12
|
-
* ## Usage
|
|
13
|
-
*
|
|
14
|
-
* ```ts
|
|
15
|
-
* import {
|
|
16
|
-
* selectInBorderedPopup,
|
|
17
|
-
* confirmInBorderedPopup,
|
|
18
|
-
* inputInBorderedPopup,
|
|
19
|
-
* } from "./ui/bordered-popups.js";
|
|
20
|
-
*
|
|
21
|
-
* const choice = await selectInBorderedPopup(ctx, {
|
|
22
|
-
* title: "Select something",
|
|
23
|
-
* items: [...],
|
|
24
|
-
* });
|
|
25
|
-
*
|
|
26
|
-
* const confirmed = await confirmInBorderedPopup(ctx, {
|
|
27
|
-
* title: "Are you sure?",
|
|
28
|
-
* });
|
|
29
|
-
*
|
|
30
|
-
* const name = await inputInBorderedPopup(ctx, {
|
|
31
|
-
* title: "Enter name",
|
|
32
|
-
* prompt: "What should we call it?",
|
|
33
|
-
* });
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* The helpers automatically handle:
|
|
37
|
-
* - Consistent 4-sided borders (╭─╮│╰╯) with stable right edge
|
|
38
|
-
* - Proper ANSI-aware padding using Pi's `truncateToWidth`
|
|
39
|
-
* - Live filtering on long lists
|
|
40
|
-
* - Back navigation ("← Go back") and Esc-to-cancel semantics
|
|
41
|
-
* - Theming consistent with the rest of Pi
|
|
42
|
-
*
|
|
43
|
-
* ## When to use
|
|
44
|
-
*
|
|
45
|
-
* Use these when you have:
|
|
46
|
-
* - Long lists that benefit from filtering
|
|
47
|
-
* - Multi-step wizards
|
|
48
|
-
* - Situations where the basic Pi UI dialogs feel too plain
|
|
49
|
-
*
|
|
50
|
-
* For very simple one-off prompts, the built-in `ctx.ui.select/input/confirm`
|
|
51
|
-
* are still perfectly acceptable and require less code.
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
55
|
-
|
|
56
|
-
// No static imports from @earendil-works/pi-tui are used for types.
|
|
57
|
-
// We rely on inference for ctx.ui.custom callback parameters (sourced via the
|
|
58
|
-
// pi-coding-agent peer) and a minimal local facade for the runtime values
|
|
59
|
-
// obtained via dynamic import. This avoids duplicate module declarations
|
|
60
|
-
// (pi-tui types nested inside coding-agent vs. direct peer) that would
|
|
61
|
-
// otherwise break tsc strict under the project's monorepo layout.
|
|
62
|
-
|
|
63
|
-
/** Internal helper to render a consistent bordered box. */
|
|
64
|
-
export function renderBorderedBox(
|
|
65
|
-
width: number,
|
|
66
|
-
title: string,
|
|
67
|
-
bodyLines: string[],
|
|
68
|
-
footer: string | undefined,
|
|
69
|
-
theme: Pick<Theme, "fg" | "bold">,
|
|
70
|
-
truncateToWidthFn: (s: string, w: number, e?: string, pad?: boolean) => string,
|
|
71
|
-
): string[] {
|
|
72
|
-
const innerWidth = Math.max(20, width - 4);
|
|
73
|
-
const top = theme.fg("accent", "╭" + "─".repeat(width - 2) + "╮");
|
|
74
|
-
const bottom = theme.fg("accent", "╰" + "─".repeat(width - 2) + "╯");
|
|
75
|
-
|
|
76
|
-
const rawTitle = theme.fg("accent", theme.bold(title));
|
|
77
|
-
const titlePadded = truncateToWidthFn(rawTitle, innerWidth, "", true);
|
|
78
|
-
const borderedTitle = theme.fg("accent", "│ ") + titlePadded + theme.fg("accent", " │");
|
|
79
|
-
|
|
80
|
-
const borderedBody = bodyLines.map((line) => {
|
|
81
|
-
const padded = truncateToWidthFn(line || "", innerWidth, "", true);
|
|
82
|
-
return theme.fg("accent", "│ ") + padded + theme.fg("accent", " │");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const lines = [top, borderedTitle, ...borderedBody];
|
|
86
|
-
|
|
87
|
-
if (footer) {
|
|
88
|
-
const rawFooter = theme.fg("dim", footer);
|
|
89
|
-
const footerPadded = truncateToWidthFn(rawFooter, innerWidth, "", true);
|
|
90
|
-
lines.push(theme.fg("accent", "│ ") + footerPadded + theme.fg("accent", " │"));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
lines.push(bottom);
|
|
94
|
-
return lines;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* High-level helper for a filterable list inside a bordered popup.
|
|
99
|
-
* Returns the chosen `.value` or `null` (on cancel / Esc).
|
|
100
|
-
*/
|
|
101
|
-
export async function selectInBorderedPopup<T = string>(
|
|
102
|
-
ctx: ExtensionContext,
|
|
103
|
-
opts: {
|
|
104
|
-
title: string;
|
|
105
|
-
items: { value: T; label: string; description?: string }[];
|
|
106
|
-
helpText?: string;
|
|
107
|
-
maxVisible?: number;
|
|
108
|
-
},
|
|
109
|
-
): Promise<T | null> {
|
|
110
|
-
const maxVis = opts.maxVisible ?? 14;
|
|
111
|
-
const help = opts.helpText ?? "↑↓ • Enter • Esc = cancel • Type to filter";
|
|
112
|
-
|
|
113
|
-
// Pure local interface (no `extends` of the real SelectList type) describing
|
|
114
|
-
// exactly the surface we use. Avoids pulling in conflicting .d.ts copies of
|
|
115
|
-
// pi-tui that exist via the coding-agent transitive dep vs. our direct peer.
|
|
116
|
-
interface SelectListHandle {
|
|
117
|
-
render(w: number): string[];
|
|
118
|
-
invalidate(): void;
|
|
119
|
-
handleInput(d: string): void;
|
|
120
|
-
onSelect: (item: { value: T }) => void;
|
|
121
|
-
onCancel: () => void;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Let inference provide the exact callback parameter types from the
|
|
125
|
-
// ExtensionCommandContext (via coding-agent peer). Explicit annotations
|
|
126
|
-
// referencing TUI/KeybindingsManager etc. from pi-tui trigger the
|
|
127
|
-
// "separate declarations of private property" tsc error in the monorepo.
|
|
128
|
-
return await ctx.ui.custom<T | null>(
|
|
129
|
-
async (tui, theme, _kb, done) => {
|
|
130
|
-
const piTui = (await import("@earendil-works/pi-tui")) as unknown as {
|
|
131
|
-
SelectList: new (
|
|
132
|
-
items: { value: T; label: string; description?: string }[],
|
|
133
|
-
maxVisible: number,
|
|
134
|
-
theme: unknown,
|
|
135
|
-
) => SelectListHandle;
|
|
136
|
-
Container: new () => { invalidate(): void };
|
|
137
|
-
truncateToWidth: (s: string, w: number, e?: string, pad?: boolean) => string;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const { SelectList, Container, truncateToWidth: truncateToWidthFn } = piTui;
|
|
141
|
-
|
|
142
|
-
let currentList: SelectListHandle | null = null;
|
|
143
|
-
|
|
144
|
-
function build() {
|
|
145
|
-
currentList = new SelectList(
|
|
146
|
-
opts.items.map((it) => ({
|
|
147
|
-
value: it.value,
|
|
148
|
-
label: it.label,
|
|
149
|
-
description: it.description,
|
|
150
|
-
})),
|
|
151
|
-
maxVis,
|
|
152
|
-
{
|
|
153
|
-
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
154
|
-
selectedText: (t: string) => theme.fg("accent", t),
|
|
155
|
-
description: (t: string) => theme.fg("muted", t),
|
|
156
|
-
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
157
|
-
noMatch: (t: string) => theme.fg("warning", t),
|
|
158
|
-
},
|
|
159
|
-
);
|
|
160
|
-
currentList.onSelect = (item) => {
|
|
161
|
-
done(item.value);
|
|
162
|
-
};
|
|
163
|
-
currentList.onCancel = () => {
|
|
164
|
-
done(null);
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
build();
|
|
169
|
-
|
|
170
|
-
const container = new Container();
|
|
171
|
-
|
|
172
|
-
const popup: {
|
|
173
|
-
render(w: number): string[];
|
|
174
|
-
invalidate(): void;
|
|
175
|
-
handleInput?(d: string): void;
|
|
176
|
-
dispose?(): void;
|
|
177
|
-
} = {
|
|
178
|
-
render(width: number) {
|
|
179
|
-
const listLines = currentList ? currentList.render(Math.max(20, width - 4)) : [];
|
|
180
|
-
return renderBorderedBox(width, opts.title, listLines, help, theme, truncateToWidthFn);
|
|
181
|
-
},
|
|
182
|
-
invalidate() {
|
|
183
|
-
container.invalidate();
|
|
184
|
-
currentList?.invalidate();
|
|
185
|
-
},
|
|
186
|
-
handleInput(d: string) {
|
|
187
|
-
currentList?.handleInput(d);
|
|
188
|
-
tui.requestRender();
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
return popup;
|
|
193
|
-
},
|
|
194
|
-
{ overlay: true },
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Yes/No (or custom labels) confirmation inside a bordered popup. */
|
|
199
|
-
export async function confirmInBorderedPopup(
|
|
200
|
-
ctx: ExtensionContext,
|
|
201
|
-
opts: {
|
|
202
|
-
title: string;
|
|
203
|
-
message?: string;
|
|
204
|
-
confirmLabel?: string;
|
|
205
|
-
cancelLabel?: string;
|
|
206
|
-
},
|
|
207
|
-
): Promise<boolean | null> {
|
|
208
|
-
const yes = opts.confirmLabel ?? "Yes";
|
|
209
|
-
const no = opts.cancelLabel ?? "No";
|
|
210
|
-
const items = [
|
|
211
|
-
{ value: true, label: yes },
|
|
212
|
-
{ value: false, label: no },
|
|
213
|
-
];
|
|
214
|
-
|
|
215
|
-
const choice = await selectInBorderedPopup(ctx, {
|
|
216
|
-
title: opts.title,
|
|
217
|
-
items,
|
|
218
|
-
helpText: "↑↓ • Enter to confirm • Esc = cancel",
|
|
219
|
-
maxVisible: 5,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
if (choice === null) return null; // user cancelled the dialog
|
|
223
|
-
return choice;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Bordered popup text input powered by Pi's Editor component.
|
|
228
|
-
* Good for free-text entry while staying inside the custom popup aesthetic.
|
|
229
|
-
*/
|
|
230
|
-
export async function inputInBorderedPopup(
|
|
231
|
-
ctx: ExtensionContext,
|
|
232
|
-
opts: {
|
|
233
|
-
title: string;
|
|
234
|
-
prompt?: string;
|
|
235
|
-
defaultValue?: string;
|
|
236
|
-
helpText?: string;
|
|
237
|
-
},
|
|
238
|
-
): Promise<string | undefined> {
|
|
239
|
-
const help = opts.helpText ?? "Enter to confirm • Esc = cancel";
|
|
240
|
-
|
|
241
|
-
// Pure local interface (no extends) for the submit hook.
|
|
242
|
-
interface EditorHandle {
|
|
243
|
-
render(w: number): string[];
|
|
244
|
-
invalidate(): void;
|
|
245
|
-
handleInput(d: string): void;
|
|
246
|
-
setText(s: string): void;
|
|
247
|
-
onSubmit: (value: string) => void;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Inference for callback params (see selectInBorderedPopup for rationale).
|
|
251
|
-
return await ctx.ui.custom<string | undefined>(
|
|
252
|
-
async (tui, theme, _kb, done) => {
|
|
253
|
-
const piTui = (await import("@earendil-works/pi-tui")) as unknown as {
|
|
254
|
-
Editor: new (tui: unknown, theme: unknown) => EditorHandle;
|
|
255
|
-
matchesKey: (data: string, key: string) => boolean;
|
|
256
|
-
truncateToWidth: (s: string, w: number, e?: string, pad?: boolean) => string;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const { Editor, matchesKey, truncateToWidth: truncateToWidthFn } = piTui;
|
|
260
|
-
|
|
261
|
-
const editorTheme = {
|
|
262
|
-
borderColor: (s: string) => theme.fg("accent", s),
|
|
263
|
-
selectList: {
|
|
264
|
-
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
265
|
-
selectedText: (t: string) => theme.fg("accent", t),
|
|
266
|
-
description: (t: string) => theme.fg("muted", t),
|
|
267
|
-
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
268
|
-
noMatch: (t: string) => theme.fg("warning", t),
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const editor = new Editor(tui, editorTheme);
|
|
273
|
-
|
|
274
|
-
if (opts.defaultValue) {
|
|
275
|
-
editor.setText(opts.defaultValue);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
editor.onSubmit = (value: string) => {
|
|
279
|
-
done(value.trim() || undefined);
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const popup: {
|
|
283
|
-
render(w: number): string[];
|
|
284
|
-
invalidate(): void;
|
|
285
|
-
handleInput?(d: string): void;
|
|
286
|
-
dispose?(): void;
|
|
287
|
-
} = {
|
|
288
|
-
render(width: number) {
|
|
289
|
-
const innerWidth = Math.max(20, width - 4);
|
|
290
|
-
const body: string[] = [];
|
|
291
|
-
|
|
292
|
-
if (opts.prompt) {
|
|
293
|
-
body.push(theme.fg("text", opts.prompt));
|
|
294
|
-
body.push("");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const editorLines = editor.render(innerWidth);
|
|
298
|
-
body.push(...editorLines);
|
|
299
|
-
|
|
300
|
-
return renderBorderedBox(width, opts.title, body, help, theme, truncateToWidthFn);
|
|
301
|
-
},
|
|
302
|
-
invalidate() {
|
|
303
|
-
editor.invalidate();
|
|
304
|
-
},
|
|
305
|
-
handleInput(data: string) {
|
|
306
|
-
if (matchesKey(data, "escape")) {
|
|
307
|
-
done(undefined);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
editor.handleInput(data);
|
|
311
|
-
tui.requestRender();
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
return popup;
|
|
316
|
-
},
|
|
317
|
-
{ overlay: true },
|
|
318
|
-
);
|
|
319
|
-
}
|