skydive-cli 0.1.0-beta.239 → 0.1.0-beta.260

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/dist/js/bin.mjs CHANGED
@@ -31,7 +31,7 @@ var __exportAll = (all, no_symbols) => {
31
31
 
32
32
  //#endregion
33
33
  //#region package.json
34
- var version$1 = "0.1.0-beta.239";
34
+ var version$1 = "0.1.0-beta.260";
35
35
 
36
36
  //#endregion
37
37
  //#region src/types.ts
@@ -2152,7 +2152,7 @@ const chatCommand = {
2152
2152
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2153
2153
  process.exit(1);
2154
2154
  }
2155
- const { runChat } = await import("./boot-CIdJ5Als.mjs");
2155
+ const { runChat } = await import("./boot-C5ztfr8K.mjs");
2156
2156
  await runChat({
2157
2157
  appUrl,
2158
2158
  sessionToken: session.value.sessionToken,
@@ -3330,7 +3330,7 @@ const switchCommand = {
3330
3330
  printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
3331
3331
  process.exit(1);
3332
3332
  }
3333
- const { runWorkspacePicker } = await import("./boot-CIdJ5Als.mjs");
3333
+ const { runWorkspacePicker } = await import("./boot-C5ztfr8K.mjs");
3334
3334
  await runWorkspacePicker(session);
3335
3335
  return;
3336
3336
  }
@@ -16,6 +16,7 @@ import os, { homedir, platform, release, tmpdir } from "node:os";
16
16
  import { createConnection } from "node:net";
17
17
  import { access, appendFile, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
18
18
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
19
+ import fuzzysort from "fuzzysort";
19
20
  import { fileURLToPath } from "node:url";
20
21
  import { fileTypeFromBuffer } from "file-type";
21
22
  import { structuredPatch } from "diff";
@@ -1240,6 +1241,68 @@ function windowStart(highlight, total, visible) {
1240
1241
  return Math.max(0, Math.min(desired, total - visible));
1241
1242
  }
1242
1243
 
1244
+ //#endregion
1245
+ //#region src/chat/tui/fuzzy.ts
1246
+ /**
1247
+ * Rank `items` against `query` across multiple keys with fuzzysort — the
1248
+ * OpenCode pattern. An empty/whitespace query returns everything in the
1249
+ * caller's original order (fuzzysort has nothing to rank); otherwise items
1250
+ * where no key matches are dropped and the rest come back best-first.
1251
+ */
1252
+ function fuzzyFilter(query, items, keys) {
1253
+ const needle = query.trim();
1254
+ if (!needle) return items.map((item) => ({
1255
+ item,
1256
+ highlights: keys.map(() => null)
1257
+ }));
1258
+ return fuzzysort.go(needle, items, { keys: [...keys] }).map((result) => ({
1259
+ item: result.obj,
1260
+ highlights: keys.map((_key, i) => {
1261
+ const indexes = result[i]?.indexes;
1262
+ return indexes !== void 0 && indexes.length > 0 ? indexes : null;
1263
+ })
1264
+ }));
1265
+ }
1266
+ /**
1267
+ * Split `text` into contiguous segments for rendering, marking the ones
1268
+ * covered by `indexes` (matched characters) so the view can color them.
1269
+ * Null/absent indexes yield a single unmatched segment.
1270
+ */
1271
+ function highlightSegments(text, indexes) {
1272
+ if (!text) return [];
1273
+ if (!indexes || indexes.length === 0) return [{
1274
+ text,
1275
+ matched: false
1276
+ }];
1277
+ const matched = new Set(indexes);
1278
+ const segments = [];
1279
+ for (let i = 0; i < text.length; i++) {
1280
+ const isMatch = matched.has(i);
1281
+ const last = segments.at(-1);
1282
+ if (last && last.matched === isMatch) last.text += text[i];
1283
+ else segments.push({
1284
+ text: text[i] ?? "",
1285
+ matched: isMatch
1286
+ });
1287
+ }
1288
+ return segments;
1289
+ }
1290
+
1291
+ //#endregion
1292
+ //#region src/chat/tui/fuzzy-text.tsx
1293
+ /**
1294
+ * Inline text with fuzzy-matched characters colored. Renders as spans, so it
1295
+ * must sit inside a <text>. `fg` styles the unmatched characters (defaults to
1296
+ * inheriting from the enclosing <text>); matches are always warning-colored,
1297
+ * which reads on both the plain and the selected (accent) row.
1298
+ */
1299
+ function FuzzyText({ text, indexes, fg }) {
1300
+ return /* @__PURE__ */ jsx(Fragment, { children: highlightSegments(text, indexes).map((segment, i) => /* @__PURE__ */ jsx("span", {
1301
+ fg: segment.matched ? theme.warning : fg,
1302
+ children: segment.text
1303
+ }, i)) });
1304
+ }
1305
+
1243
1306
  //#endregion
1244
1307
  //#region src/chat/tui/screens/agent-picker.tsx
1245
1308
  function AgentPickerScreen() {
@@ -1332,12 +1395,12 @@ function FilterableAgentList({ agents, scope, onPick }) {
1332
1395
  useEffect(() => {
1333
1396
  setHighlight(0);
1334
1397
  }, [scope]);
1335
- const q = query.trim().toLowerCase();
1336
- const filtered = q ? agents.filter((a) => agentHaystack(a).includes(q)) : agents;
1398
+ const hits = fuzzyFilter(query, agents, agentKeys);
1399
+ const filtered = hits.map((h) => h.item);
1337
1400
  const clamped = Math.min(highlight, Math.max(0, filtered.length - 1));
1338
1401
  const visibleRows = Math.max(3, height - 6);
1339
1402
  const start = windowStart(clamped, filtered.length, visibleRows);
1340
- const windowed = filtered.slice(start, start + visibleRows);
1403
+ const windowed = hits.slice(start, start + visibleRows);
1341
1404
  useKeyboard((key) => {
1342
1405
  if (key.name === "up") setHighlight(Math.max(0, clamped - 1));
1343
1406
  else if (key.name === "down") setHighlight(Math.min(Math.max(0, filtered.length - 1), clamped + 1));
@@ -1399,28 +1462,33 @@ function FilterableAgentList({ agents, scope, onPick }) {
1399
1462
  query,
1400
1463
  "”"
1401
1464
  ]
1402
- }) : windowed.map((a, i) => {
1465
+ }) : windowed.map((hit, i) => {
1466
+ const a = hit.item;
1403
1467
  const selected = start + i === clamped;
1404
1468
  return /* @__PURE__ */ jsxs("text", {
1405
1469
  fg: selected ? theme.accent : theme.fg,
1406
- children: [selected ? "› " : " ", agentLabel(a)]
1470
+ children: [
1471
+ selected ? "› " : " ",
1472
+ /* @__PURE__ */ jsx(FuzzyText, {
1473
+ text: a.name,
1474
+ indexes: hit.highlights[0]
1475
+ }),
1476
+ a.slug ? /* @__PURE__ */ jsxs(Fragment, { children: [" @", /* @__PURE__ */ jsx(FuzzyText, {
1477
+ text: a.slug,
1478
+ indexes: hit.highlights[1]
1479
+ })] }) : null
1480
+ ]
1407
1481
  }, a.id);
1408
1482
  })
1409
1483
  })
1410
1484
  ]
1411
1485
  });
1412
1486
  }
1413
- function agentHaystack(a) {
1414
- return [
1415
- a.name,
1416
- a.slug ?? "",
1417
- a.title ?? ""
1418
- ].join(" ").toLowerCase();
1419
- }
1420
- function agentLabel(a) {
1421
- if (a.slug) return `${a.name} @${a.slug}`;
1422
- return a.name;
1423
- }
1487
+ const agentKeys = [
1488
+ (a) => a.name,
1489
+ (a) => a.slug ?? "",
1490
+ (a) => a.title ?? ""
1491
+ ];
1424
1492
 
1425
1493
  //#endregion
1426
1494
  //#region src/chat/tui/agent-name.ts
@@ -1755,11 +1823,10 @@ function ConversationList({ agent, conversations, onPick }) {
1755
1823
  const [list, setList] = useState(conversations);
1756
1824
  const [confirmId, setConfirmId] = useState(null);
1757
1825
  const [error, setError] = useState(null);
1758
- const q = query.trim().toLowerCase();
1759
- const filtered = q ? list.filter((c) => conversationHaystack(c).includes(q)) : list;
1760
- const rows = [{ kind: "new" }, ...filtered.map((conv) => ({
1826
+ const hits = fuzzyFilter(query, list, conversationKeys);
1827
+ const rows = [{ kind: "new" }, ...hits.map((hit) => ({
1761
1828
  kind: "conv",
1762
- conv
1829
+ hit
1763
1830
  }))];
1764
1831
  const clamped = Math.min(highlight, Math.max(0, rows.length - 1));
1765
1832
  const visibleRows = Math.max(3, height - 6);
@@ -1777,7 +1844,7 @@ function ConversationList({ agent, conversations, onPick }) {
1777
1844
  else onPick({
1778
1845
  kind: "chat",
1779
1846
  agent,
1780
- conversation: row.conv
1847
+ conversation: row.hit.item
1781
1848
  });
1782
1849
  };
1783
1850
  const deleteConversation = (id) => {
@@ -1804,7 +1871,7 @@ function ConversationList({ agent, conversations, onPick }) {
1804
1871
  const row = rows[clamped];
1805
1872
  if (row?.kind === "conv") {
1806
1873
  setError(null);
1807
- setConfirmId(row.conv.id);
1874
+ setConfirmId(row.hit.item.id);
1808
1875
  }
1809
1876
  } else if (key.name === "return") {
1810
1877
  const row = rows[clamped];
@@ -1849,7 +1916,7 @@ function ConversationList({ agent, conversations, onPick }) {
1849
1916
  }) : /* @__PURE__ */ jsxs("text", {
1850
1917
  fg: theme.dim,
1851
1918
  children: [
1852
- filtered.length,
1919
+ hits.length,
1853
1920
  "/",
1854
1921
  list.length,
1855
1922
  " · ↑/↓ move · ↵ open · ctrl+d delete · esc back"
@@ -1871,21 +1938,22 @@ function ConversationList({ agent, conversations, onPick }) {
1871
1938
  fg,
1872
1939
  children: [
1873
1940
  selected ? "› " : " ",
1874
- row.conv.title ?? "(untitled)",
1941
+ /* @__PURE__ */ jsx(FuzzyText, {
1942
+ text: row.hit.item.title ?? "(untitled)",
1943
+ indexes: row.hit.item.title ? row.hit.highlights[0] : null
1944
+ }),
1875
1945
  /* @__PURE__ */ jsxs("span", {
1876
1946
  fg: theme.dim,
1877
- children: [" ", previewLine(row.conv)]
1947
+ children: [" ", previewLine(row.hit.item)]
1878
1948
  })
1879
1949
  ]
1880
- }, row.conv.id);
1950
+ }, row.hit.item.id);
1881
1951
  })
1882
1952
  })
1883
1953
  ]
1884
1954
  });
1885
1955
  }
1886
- function conversationHaystack(c) {
1887
- return [c.title ?? "", c.preview ?? ""].join(" ").toLowerCase();
1888
- }
1956
+ const conversationKeys = [(c) => c.title ?? "", (c) => c.preview ?? ""];
1889
1957
  function previewLine(c) {
1890
1958
  const when = relativeTime(c.updatedAt);
1891
1959
  const prefix = c.preview ? c.preview.replace(/\s+/g, " ").trim() : "";
@@ -4311,12 +4379,12 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
4311
4379
  const [saving, setSaving] = useState(false);
4312
4380
  const [saveError, setSaveError] = useState(null);
4313
4381
  const rest = useStore((s) => s.rest);
4314
- const q = query.trim().toLowerCase();
4315
- const filtered = q ? models.filter((m) => modelHaystack(m).includes(q)) : models;
4382
+ const hits = fuzzyFilter(query, models, modelKeys);
4383
+ const filtered = hits.map((h) => h.item);
4316
4384
  const clamped = Math.min(highlight, Math.max(0, filtered.length - 1));
4317
4385
  const visibleRows = Math.max(3, height - 8);
4318
4386
  const start = windowStart(clamped, filtered.length, visibleRows);
4319
- const windowed = filtered.slice(start, start + visibleRows);
4387
+ const windowed = hits.slice(start, start + visibleRows);
4320
4388
  async function select(model) {
4321
4389
  if (!rest || saving) return;
4322
4390
  if (model.id === currentModel) {
@@ -4395,7 +4463,8 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
4395
4463
  query,
4396
4464
  "”"
4397
4465
  ]
4398
- }) : windowed.map((m, i) => {
4466
+ }) : windowed.map((hit, i) => {
4467
+ const m = hit.item;
4399
4468
  const selected = start + i === clamped;
4400
4469
  const isCurrent = m.id === currentModel;
4401
4470
  return /* @__PURE__ */ jsxs("text", {
@@ -4403,7 +4472,19 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
4403
4472
  children: [
4404
4473
  selected ? "› " : " ",
4405
4474
  isCurrent ? "● " : " ",
4406
- modelLabel(m)
4475
+ m.providerDisplay ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FuzzyText, {
4476
+ text: m.providerDisplay,
4477
+ indexes: hit.highlights[1]
4478
+ }), " · "] }) : null,
4479
+ /* @__PURE__ */ jsx(FuzzyText, {
4480
+ text: m.displayName,
4481
+ indexes: hit.highlights[0]
4482
+ }),
4483
+ " ",
4484
+ /* @__PURE__ */ jsx(FuzzyText, {
4485
+ text: m.id,
4486
+ indexes: hit.highlights[2]
4487
+ })
4407
4488
  ]
4408
4489
  }, m.id);
4409
4490
  })
@@ -4415,16 +4496,11 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
4415
4496
  ]
4416
4497
  });
4417
4498
  }
4418
- function modelHaystack(m) {
4419
- return [
4420
- m.displayName,
4421
- m.providerDisplay ?? "",
4422
- m.id
4423
- ].join(" ").toLowerCase();
4424
- }
4425
- function modelLabel(m) {
4426
- return `${m.providerDisplay ? `${m.providerDisplay} · ` : ""}${m.displayName} ${m.id}`;
4427
- }
4499
+ const modelKeys = [
4500
+ (m) => m.displayName,
4501
+ (m) => m.providerDisplay ?? "",
4502
+ (m) => m.id
4503
+ ];
4428
4504
  /**
4429
4505
  * The REST error routes return `{ "error": "message" }`. Pull that out for a
4430
4506
  * human-readable status line; fall back to the raw body on anything else.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.239",
3
+ "version": "0.1.0-beta.260",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -33,6 +33,7 @@
33
33
  "diff": "9.0.0",
34
34
  "eventsource-parser": "^3.0.8",
35
35
  "file-type": "^21.3.4",
36
+ "fuzzysort": "^3.1.0",
36
37
  "neverthrow": "^8.2.0",
37
38
  "open": "^10.1.0",
38
39
  "react": "^19.0.0",