pi-connect 0.1.2 → 0.1.3

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 (2) hide show
  1. package/index.ts +82 -22
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DynamicBorder, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { getEnvApiKey } from "@mariozechner/pi-ai";
3
- import { Container, SelectList, Text, type SelectItem } from "@mariozechner/pi-tui";
3
+ import { Container, fuzzyFilter, Input, Key, matchesKey, SelectList, Text, type SelectItem } from "@mariozechner/pi-tui";
4
4
  import { exec as execCb } from "node:child_process";
5
5
 
6
6
  const DISPLAY_NAME_OVERRIDES: Record<string, string> = {
@@ -113,30 +113,90 @@ async function pickItem(ctx: any, title: string, subtitle: string | undefined, i
113
113
  container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
114
114
  container.addChild(new Text(theme.fg("text", theme.bold(title)), 1, 0));
115
115
  if (subtitle) container.addChild(new Text(theme.fg("dim", subtitle), 1, 0));
116
+
117
+ container.addChild(new Text(theme.fg("muted", "Search"), 1, 0));
118
+ const searchInput = new Input();
119
+ searchInput.focused = true;
120
+ container.addChild(searchInput);
116
121
  container.addChild(new Text("", 0, 0));
117
122
 
118
- const maxVisible = Math.max(6, Math.min(items.length, Math.floor((tui.terminal.rows - 12) / 2)));
119
- const list = new SelectList(items, maxVisible, {
120
- selectedPrefix: (t) => theme.fg("accent", t),
121
- selectedText: (t) => theme.fg("accent", theme.bold(t)),
122
- description: (t) => theme.fg("muted", t),
123
- scrollInfo: (t) => theme.fg("dim", t),
124
- noMatch: (t) => theme.fg("warning", t),
125
- });
126
- list.onSelect = (item) => done(item);
127
- list.onCancel = () => done(null);
128
- container.addChild(list);
123
+ const maxVisible = Math.max(6, Math.min(items.length, Math.floor((tui.terminal.rows - 14) / 2)));
124
+ const listContainer = new Container();
125
+ container.addChild(listContainer);
126
+
127
+ let list: SelectList;
128
+
129
+ const sectionOauth = items.find((item) => item.value === "__section_oauth");
130
+ const sectionApi = items.find((item) => item.value === "__section_api");
131
+ const itemTheme = {
132
+ selectedPrefix: (t: string) => theme.fg("accent", t),
133
+ selectedText: (t: string) => theme.fg("accent", theme.bold(t)),
134
+ description: (t: string) => theme.fg("muted", t),
135
+ scrollInfo: (t: string) => theme.fg("dim", t),
136
+ noMatch: (t: string) => theme.fg("warning", t),
137
+ };
138
+
139
+ const rebuildList = () => {
140
+ const query = searchInput.getValue().trim();
141
+ const normalItems = items.filter((item) => !item.value.startsWith("__section_"));
142
+ const filtered = query
143
+ ? fuzzyFilter(normalItems, query, (item) => `${item.label} ${item.description ?? ""}`)
144
+ : normalItems;
145
+
146
+ const oauthItems = filtered.filter((item) => item.value.startsWith("oauth:"));
147
+ const apiItems = filtered.filter((item) => item.value.startsWith("api:"));
148
+
149
+ const displayItems: SelectItem[] = [];
150
+ if (oauthItems.length > 0 && sectionOauth) displayItems.push(sectionOauth);
151
+ displayItems.push(...oauthItems);
152
+ if (apiItems.length > 0 && sectionApi) displayItems.push(sectionApi);
153
+ displayItems.push(...apiItems);
154
+
155
+ const finalItems = displayItems.length > 0
156
+ ? displayItems
157
+ : [{ value: "__empty", label: "No matching providers", description: "Try a different search" }];
158
+
159
+ list = new SelectList(finalItems, maxVisible, itemTheme);
160
+ list.onSelect = (item) => {
161
+ if (item.value.startsWith("__section_") || item.value === "__empty") return;
162
+ done(item);
163
+ };
164
+ list.onCancel = () => done(null);
165
+
166
+ listContainer.clear();
167
+ listContainer.addChild(list);
168
+ };
169
+
170
+ rebuildList();
129
171
 
130
172
  container.addChild(new Text("", 0, 0));
131
173
  container.addChild(new Text(`${theme.fg("success", "●")} connected ${theme.fg("warning", "◌")} env ${theme.fg("muted", "○")} new`, 1, 0));
132
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate • Enter select • Esc cancel"), 1, 0));
174
+ container.addChild(new Text(theme.fg("dim", "type to search • ↑↓ navigate • Enter select • Esc cancel/clear"), 1, 0));
133
175
  container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
134
176
 
135
177
  return {
136
178
  render: (w) => container.render(w),
137
179
  invalidate: () => container.invalidate(),
138
180
  handleInput: (data) => {
139
- list.handleInput(data);
181
+ if (matchesKey(data, Key.escape)) {
182
+ if (searchInput.getValue()) {
183
+ searchInput.setValue("");
184
+ rebuildList();
185
+ tui.requestRender();
186
+ return;
187
+ }
188
+ done(null);
189
+ return;
190
+ }
191
+
192
+ if (matchesKey(data, Key.up) || matchesKey(data, Key.down) || matchesKey(data, Key.enter) || matchesKey(data, Key.return) || matchesKey(data, Key.pageUp) || matchesKey(data, Key.pageDown)) {
193
+ list.handleInput(data);
194
+ tui.requestRender();
195
+ return;
196
+ }
197
+
198
+ searchInput.handleInput(data);
199
+ rebuildList();
140
200
  tui.requestRender();
141
201
  },
142
202
  };
@@ -150,9 +210,9 @@ export default function piConnectExtension(pi: ExtensionAPI) {
150
210
  const apiProviderIds = getApiCapableProviderIds(ctx);
151
211
 
152
212
  const statusIcon = (providerId: string) => {
153
- if (authStorage.has(providerId)) return ctx.ui.theme.fg("success", "●");
154
- if (getEnvApiKey(providerId)) return ctx.ui.theme.fg("warning", "◌");
155
- return ctx.ui.theme.fg("muted", "○");
213
+ if (authStorage.has(providerId)) return "●";
214
+ if (getEnvApiKey(providerId)) return "◌";
215
+ return "○";
156
216
  };
157
217
 
158
218
  const items: SelectItem[] = [];
@@ -160,7 +220,7 @@ export default function piConnectExtension(pi: ExtensionAPI) {
160
220
  if (oauthProviders.length > 0) {
161
221
  items.push({
162
222
  value: "__section_oauth",
163
- label: ctx.ui.theme.bold("OAuth providers"),
223
+ label: "OAuth providers",
164
224
  description: "login via browser"
165
225
  });
166
226
  for (const provider of oauthProviders) {
@@ -175,7 +235,7 @@ export default function piConnectExtension(pi: ExtensionAPI) {
175
235
  if (apiProviderIds.length > 0) {
176
236
  items.push({
177
237
  value: "__section_api",
178
- label: ctx.ui.theme.bold("API key providers"),
238
+ label: "API key providers",
179
239
  description: "paste and save key"
180
240
  });
181
241
  for (const providerId of apiProviderIds) {
@@ -243,8 +303,8 @@ export default function piConnectExtension(pi: ExtensionAPI) {
243
303
 
244
304
  if (oauthIds.has(providerId) && apiIds.has(providerId)) {
245
305
  const method = await pickItem(ctx, prettyProviderName(providerId), "Choose how to connect", [
246
- { value: "oauth", label: ctx.ui.theme.bold("OAuth"), description: "browser login" },
247
- { value: "api", label: ctx.ui.theme.bold("API key"), description: "paste and save key" },
306
+ { value: "oauth", label: "OAuth", description: "browser login" },
307
+ { value: "api", label: "API key", description: "paste and save key" },
248
308
  ]);
249
309
  if (!method) return;
250
310
  if (method.value === "oauth") {
@@ -275,7 +335,7 @@ export default function piConnectExtension(pi: ExtensionAPI) {
275
335
  }
276
336
  const items: SelectItem[] = providers.map((providerId) => ({
277
337
  value: providerId,
278
- label: `${ctx.ui.theme.fg("success", "●")} ${ctx.ui.theme.bold(prettyProviderName(providerId))}`,
338
+ label: `● ${prettyProviderName(providerId)}`,
279
339
  description: "Connected"
280
340
  }));
281
341
  const selected = await pickItem(ctx, "Disconnect provider", "Remove a saved credential", items);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-connect",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Unified OAuth and API key login for pi with an OpenCode-inspired UI. Connect 15+ providers with one /connect command.",
5
5
  "type": "module",
6
6
  "license": "MIT",