turbollm 1.6.2 → 1.6.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 (81) hide show
  1. package/README.md +7 -3
  2. package/dist/cli.js +203 -62
  3. package/dist/webdist/assets/{AgentsScreen-BviDj463.js → AgentsScreen-DSJoisHr.js} +1 -1
  4. package/dist/webdist/assets/{ChatScreen-4a_6ZX6c.js → ChatScreen-CPTQTPDq.js} +3 -3
  5. package/dist/webdist/assets/{CustomizeScreen-BhZJK7xv.js → CustomizeScreen-CveMZwK6.js} +1 -1
  6. package/dist/webdist/assets/{DeveloperScreen-8CVekggw.js → DeveloperScreen-fnTDP0tf.js} +1 -1
  7. package/dist/webdist/assets/{EnginesScreen-zHLzht8k.js → EnginesScreen-rlz0E1mk.js} +1 -1
  8. package/dist/webdist/assets/{ModelDetailDialog-DjMSf0rz.js → ModelDetailDialog-b1xxFqb8.js} +1 -1
  9. package/dist/webdist/assets/{ModelDirs-CIOcl5qu.js → ModelDirs-DPJoCOcI.js} +1 -1
  10. package/dist/webdist/assets/{ModelsScreen-v7kDcWmr.js → ModelsScreen-BQF-ZHhj.js} +2 -2
  11. package/dist/webdist/assets/{SettingsScreen-DnSgBps7.js → SettingsScreen-Z4AoirUg.js} +1 -1
  12. package/dist/webdist/assets/{alert-dialog-93rFXd1_.js → alert-dialog-DzOwQljM.js} +1 -1
  13. package/dist/webdist/assets/{arc-BFc9YP9w.js → arc-DJj3Lyu5.js} +1 -1
  14. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-Bd6WFbHz.js → architectureDiagram-3BPJPVTR-BcgkjjsC.js} +1 -1
  15. package/dist/webdist/assets/{arrow-left--NjoPnor.js → arrow-left-C8---sig.js} +1 -1
  16. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-Dupawk92.js → blockDiagram-GPEHLZMM-C5iJ2c-N.js} +1 -1
  17. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-BMhAUE8i.js → c4Diagram-AAUBKEIU-BnsiE4U5.js} +1 -1
  18. package/dist/webdist/assets/channel-DidzH_JR.js +1 -0
  19. package/dist/webdist/assets/check-CRM4LOGX.js +1 -0
  20. package/dist/webdist/assets/{chevron-right-kiIQKgde.js → chevron-right-C18nBpLx.js} +1 -1
  21. package/dist/webdist/assets/{chunk-2J33WTMH-BhxySqcq.js → chunk-2J33WTMH-D_Hm4Lii.js} +1 -1
  22. package/dist/webdist/assets/{chunk-4BX2VUAB-B-UGkx88.js → chunk-4BX2VUAB-Cooqn_zH.js} +1 -1
  23. package/dist/webdist/assets/{chunk-55IACEB6-BDvjmk8T.js → chunk-55IACEB6-C2XDZnC0.js} +1 -1
  24. package/dist/webdist/assets/{chunk-727SXJPM-BhyFFQxZ.js → chunk-727SXJPM-CCEeI48H.js} +1 -1
  25. package/dist/webdist/assets/{chunk-AQP2D5EJ-VTSW3fZJ.js → chunk-AQP2D5EJ-7FdU3GXV.js} +1 -1
  26. package/dist/webdist/assets/{chunk-FMBD7UC4-QKx1b_-m.js → chunk-FMBD7UC4-Dpyt8woB.js} +1 -1
  27. package/dist/webdist/assets/{chunk-ND2GUHAM-DZANzpY0.js → chunk-ND2GUHAM-ildY-Wx8.js} +1 -1
  28. package/dist/webdist/assets/{chunk-QZHKN3VN-DVZIy115.js → chunk-QZHKN3VN-DfdDD-h6.js} +1 -1
  29. package/dist/webdist/assets/{circle-check-C4aACGyb.js → circle-check-C9nr7e7f.js} +1 -1
  30. package/dist/webdist/assets/{circle-x-koFsrlGA.js → circle-x-CrulSiVz.js} +1 -1
  31. package/dist/webdist/assets/classDiagram-4FO5ZUOK-DrgybMsY.js +1 -0
  32. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-DrgybMsY.js +1 -0
  33. package/dist/webdist/assets/{common-CQVogVIs.js → common-B6XHnwTG.js} +1 -1
  34. package/dist/webdist/assets/{copy-button-3_2Us2zb.js → copy-button-Sp-U8ULJ.js} +1 -1
  35. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-Ci07N3_4.js → cose-bilkent-S5V4N54A-DggDgWVR.js} +1 -1
  36. package/dist/webdist/assets/{dagre-BM42HDAG-45qc0phu.js → dagre-BM42HDAG-C3owUH7E.js} +1 -1
  37. package/dist/webdist/assets/{diagram-2AECGRRQ-DA3u3hzE.js → diagram-2AECGRRQ-Ch2E5js7.js} +1 -1
  38. package/dist/webdist/assets/{diagram-5GNKFQAL-B3wpvPKp.js → diagram-5GNKFQAL-DLeGX2xz.js} +1 -1
  39. package/dist/webdist/assets/{diagram-KO2AKTUF-h1qR3ckq.js → diagram-KO2AKTUF-DpxkKgZH.js} +1 -1
  40. package/dist/webdist/assets/{diagram-LMA3HP47-B3uU2roT.js → diagram-LMA3HP47-ia4luRgC.js} +1 -1
  41. package/dist/webdist/assets/{diagram-OG6HWLK6-CibVtZrO.js → diagram-OG6HWLK6-BMmkGNMK.js} +1 -1
  42. package/dist/webdist/assets/{dialog-Dn3dxmMP.js → dialog-DLncpxeJ.js} +1 -1
  43. package/dist/webdist/assets/{erDiagram-TEJ5UH35-VsHZBAOZ.js → erDiagram-TEJ5UH35-DkKBBG05.js} +1 -1
  44. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-DhYah8sF.js → flowDiagram-I6XJVG4X-kUtoUBI6.js} +1 -1
  45. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-RPkNEJnH.js → ganttDiagram-6RSMTGT7-DQReYR3Z.js} +1 -1
  46. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-JknfsYQp.js → gitGraphDiagram-PVQCEYII-B2kU7JBk.js} +1 -1
  47. package/dist/webdist/assets/{index-K2ZQBdky.js → index-CjGc01NF.js} +2 -2
  48. package/dist/webdist/assets/{infoDiagram-5YYISTIA-BeGMPySd.js → infoDiagram-5YYISTIA-k1S105HR.js} +1 -1
  49. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-D38Q1-tz.js → ishikawaDiagram-YF4QCWOH-BgPQRD_p.js} +1 -1
  50. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-CG8TFzp6.js → journeyDiagram-JHISSGLW-_aNOepbu.js} +1 -1
  51. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-CNgUZ1rd.js → kanban-definition-UN3LZRKU-ooXgg2sY.js} +1 -1
  52. package/dist/webdist/assets/{linear-DYIxmEeo.js → linear-B6ijN2hK.js} +1 -1
  53. package/dist/webdist/assets/{mermaid.core-CZhJJFjL.js → mermaid.core-C39fo6yP.js} +4 -4
  54. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-BIVYSbvh.js → mindmap-definition-RKZ34NQL-Ci1PW-rt.js} +1 -1
  55. package/dist/webdist/assets/{pencil-iVnKKeCp.js → pencil-CvIU0pkN.js} +1 -1
  56. package/dist/webdist/assets/{personas-DslX_qdt.js → personas-CIW1sEuT.js} +2 -0
  57. package/dist/webdist/assets/{pieDiagram-4H26LBE5-CU9qALXo.js → pieDiagram-4H26LBE5-SVL4rTNg.js} +1 -1
  58. package/dist/webdist/assets/{plus-mSm32jpN.js → plus-3YM8ko_h.js} +1 -1
  59. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-AE50YmvR.js → quadrantDiagram-W4KKPZXB-sTVWSdlQ.js} +1 -1
  60. package/dist/webdist/assets/{refresh-cw-C04dn1r1.js → refresh-cw-BrUDSVRs.js} +1 -1
  61. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-Crymka5P.js → requirementDiagram-4Y6WPE33-3R4K8jZs.js} +1 -1
  62. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-DRviwCen.js → sankeyDiagram-5OEKKPKP-BMozn5XV.js} +1 -1
  63. package/dist/webdist/assets/{save-BT2nzRB6.js → save-CeI7y6CK.js} +1 -1
  64. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-DuBllsfJ.js → sequenceDiagram-3UESZ5HK-B0bhq6qo.js} +1 -1
  65. package/dist/webdist/assets/{skeleton-BfMJjCGZ.js → skeleton-Dm2BPYbG.js} +1 -1
  66. package/dist/webdist/assets/{stateDiagram-AJRCARHV-Cj4BwRLl.js → stateDiagram-AJRCARHV-CXaf-H6k.js} +1 -1
  67. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-BYFRKnq-.js +1 -0
  68. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-BWnH2-RY.js → timeline-definition-PNZ67QCA-BUO22RTP.js} +1 -1
  69. package/dist/webdist/assets/{trash-2-Ce39UFy5.js → trash-2-D1fUAajM.js} +1 -1
  70. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-GgdSx9c7.js → vennDiagram-CIIHVFJN-B6l8WK-i.js} +1 -1
  71. package/dist/webdist/assets/{wardley-L42UT6IY-C9XKbMb6.js → wardley-L42UT6IY-nBQZIDHO.js} +1 -1
  72. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-ZxceNtWm.js → wardleyDiagram-YWT4CUSO-Bl5nA5Sj.js} +1 -1
  73. package/dist/webdist/assets/{wrench-Ceqe74n2.js → wrench-2q903Ji-.js} +1 -1
  74. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-C-mAbbHW.js → xychartDiagram-2RQKCTM6-BfRB7FkA.js} +1 -1
  75. package/dist/webdist/index.html +1 -1
  76. package/package.json +1 -1
  77. package/dist/webdist/assets/channel-BCSKocRg.js +0 -1
  78. package/dist/webdist/assets/check-Bu2UFiB0.js +0 -1
  79. package/dist/webdist/assets/classDiagram-4FO5ZUOK-B_Eb_jRj.js +0 -1
  80. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-B_Eb_jRj.js +0 -1
  81. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-Y25sKBZm.js +0 -1
package/README.md CHANGED
@@ -138,10 +138,14 @@ the detail:
138
138
  metadata (arch, params, quant, context, vision) for every file.
139
139
  - **Browse & download from Hugging Face**, in-app: a live, sortable list (trending / downloads
140
140
  / likes / recently updated / newest) alongside a permanent detail pane — pick a quant, read
141
- the **rendered model card**, and download with **resume + SHA-256 verification**. Gated
142
- models (Llama, Gemma) work via your own HF token, which **never leaves your machine**.
141
+ the **rendered model card**, and download with **resume + SHA-256 verification**. Each model
142
+ lands in its own folder (mirroring Hugging Face's own layout) with its **vision projector and
143
+ every shard of a split/multipart quant fetched alongside it automatically**. Gated models
144
+ (Llama, Gemma) work via your own HF token, which **never leaves your machine**.
143
145
  - **Import from any URL** — not just Hugging Face. Paste a direct `.gguf` link (model-author
144
- sites, mirrors, private servers); it disk-space-checks and downloads through the same manager.
146
+ sites, mirrors, private servers) and it disk-space-checks and downloads through the same
147
+ manager; paste a Hugging Face **model page** link instead and it opens that repo's quant
148
+ picker so you can choose which one to download.
145
149
  - **Quant recommendation per GPU** and a **VRAM-fit verdict** so you pick a quant that
146
150
  actually fits before you commit.
147
151
  - **Primary download folder**, real-time **measured t/s per model**, and **delete-from-disk**.
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { spawn as spawn5 } from "child_process";
5
5
  import { openSync as openSync3, readFileSync as readFileSync11 } from "fs";
6
6
  import { serve } from "@hono/node-server";
7
- import { dirname as dirname9, join as join24 } from "path";
7
+ import { dirname as dirname10, join as join24 } from "path";
8
8
  import { fileURLToPath as fileURLToPath2 } from "url";
9
9
 
10
10
  // src/config/config.ts
@@ -4247,6 +4247,54 @@ var HfClient = class {
4247
4247
  ...safetensors ? { safetensors } : {}
4248
4248
  };
4249
4249
  }
4250
+ /** Expand a chosen GGUF into every concrete file needed for a working model
4251
+ * (spec 10 §3): all shards of its split group (NNNNN-of-NNNNN) plus its mmproj vision
4252
+ * projector. Matching is scoped to the chosen file's OWN repo directory so a repo
4253
+ * that organises quants in per-quant subfolders (e.g. `Q4_K_M/…`, `Q8_0/…`) never
4254
+ * cross-matches another quant's identically-named shards. The mmproj is preferred
4255
+ * from the same directory (mirrors the scanner's per-directory pairing), falling back
4256
+ * to the largest anywhere in the repo. `rev` is the git revision (branch/tag/commit)
4257
+ * the download targets. Best-effort: on any HF failure it returns just the one
4258
+ * requested file. When the requested file is itself an mmproj, none is paired. */
4259
+ async expandModelFiles(repo, rfilename, rev = "main") {
4260
+ const wantBase = base(rfilename).toLowerCase();
4261
+ let tree;
4262
+ try {
4263
+ tree = await this.getJson(`${BASE}/api/models/${repo}/tree/${encodeURIComponent(rev)}?recursive=true`);
4264
+ } catch {
4265
+ return { dir: dirOf(rfilename), files: [{ rfilename, size: 0, mmproj: wantBase.includes("mmproj") }] };
4266
+ }
4267
+ const ggufs = tree.filter((e) => e.type === "file" && /\.gguf$/i.test(e.path));
4268
+ const chosen = ggufs.find((e) => base(e.path).toLowerCase() === wantBase);
4269
+ if (!chosen) {
4270
+ return { dir: dirOf(rfilename), files: [{ rfilename, size: 0, mmproj: wantBase.includes("mmproj") }] };
4271
+ }
4272
+ const modelDir = dirOf(chosen.path);
4273
+ const inModelDir = (e) => dirOf(e.path) === modelDir;
4274
+ const files = [];
4275
+ const split = base(chosen.path).match(SPLIT_RE2);
4276
+ if (split) {
4277
+ const prefix = split[1].toLowerCase();
4278
+ const total = split[3];
4279
+ for (const e of ggufs) {
4280
+ if (!inModelDir(e)) continue;
4281
+ const sm = base(e.path).match(SPLIT_RE2);
4282
+ if (sm && sm[1].toLowerCase() === prefix && sm[3] === total) {
4283
+ files.push({ rfilename: e.path, size: sizeOf(e), sha256: e.lfs?.oid, mmproj: false });
4284
+ }
4285
+ }
4286
+ files.sort((a, b) => a.rfilename.localeCompare(b.rfilename));
4287
+ } else {
4288
+ files.push({ rfilename: chosen.path, size: sizeOf(chosen), sha256: chosen.lfs?.oid, mmproj: false });
4289
+ }
4290
+ if (!wantBase.includes("mmproj")) {
4291
+ const projectors = ggufs.filter((e) => base(e.path).toLowerCase().includes("mmproj"));
4292
+ const sameDir = projectors.filter(inModelDir);
4293
+ const best = (sameDir.length ? sameDir : projectors).sort((a, b) => sizeOf(b) - sizeOf(a))[0];
4294
+ if (best) files.push({ rfilename: best.path, size: sizeOf(best), sha256: best.lfs?.oid, mmproj: true });
4295
+ }
4296
+ return { dir: modelDir, files };
4297
+ }
4250
4298
  /** Public model-card fetch (ADR-099): the cleaned README for `owner/repo`, or '' when
4251
4299
  * missing/unreachable. Used by auto-tune to read recommended sampling from the card.
4252
4300
  * Uses a LARGER cap than the display path: popular requanters (e.g. unsloth) put their
@@ -4397,6 +4445,10 @@ function base(p) {
4397
4445
  const i = p.lastIndexOf("/");
4398
4446
  return i >= 0 ? p.slice(i + 1) : p;
4399
4447
  }
4448
+ function dirOf(p) {
4449
+ const i = p.lastIndexOf("/");
4450
+ return i >= 0 ? p.slice(0, i) : "";
4451
+ }
4400
4452
 
4401
4453
  // src/downloads/downloads.ts
4402
4454
  import { createHash as createHash3 } from "crypto";
@@ -4411,11 +4463,10 @@ import {
4411
4463
  statSync as statSync3,
4412
4464
  writeFileSync as writeFileSync5
4413
4465
  } from "fs";
4414
- import { basename as basename2, join as join15 } from "path";
4466
+ import { basename as basename2, dirname as dirname5, join as join15 } from "path";
4415
4467
  import { Readable as Readable2 } from "stream";
4416
4468
  import { pipeline as pipeline2 } from "stream/promises";
4417
4469
  var MAX_CONCURRENT = 2;
4418
- var HF_BLOB_RE = /^https?:\/\/huggingface\.co\/.+\/resolve\/.+\.gguf$/i;
4419
4470
  var DownloadError = class extends Error {
4420
4471
  constructor(code, message) {
4421
4472
  super(message);
@@ -4425,10 +4476,11 @@ var DownloadError = class extends Error {
4425
4476
  code;
4426
4477
  };
4427
4478
  var DownloadManager = class {
4428
- constructor(store2, onComplete, authHeaders) {
4479
+ constructor(store2, onComplete, authHeaders, expand) {
4429
4480
  this.store = store2;
4430
4481
  this.onComplete = onComplete;
4431
4482
  this.authHeaders = authHeaders;
4483
+ this.expand = expand;
4432
4484
  const dir = join15(store2.dir(), "downloads");
4433
4485
  mkdirSync8(dir, { recursive: true });
4434
4486
  this.manifestPath = join15(dir, "manifest.json");
@@ -4439,6 +4491,7 @@ var DownloadManager = class {
4439
4491
  store;
4440
4492
  onComplete;
4441
4493
  authHeaders;
4494
+ expand;
4442
4495
  records = /* @__PURE__ */ new Map();
4443
4496
  controllers = /* @__PURE__ */ new Map();
4444
4497
  manifestPath;
@@ -4460,57 +4513,115 @@ var DownloadManager = class {
4460
4513
  list() {
4461
4514
  return [...this.records.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
4462
4515
  }
4463
- /** Enqueue a download. Validates the target + disk space, persists, then kicks
4464
- * the queue. Throws DownloadError for caller-actionable failures (no dir set,
4465
- * bad URL, insufficient disk). */
4466
- enqueue(input) {
4516
+ /** Enqueue a download. Validates the target + disk space, persists, then kicks the
4517
+ * queue. Returns every record created — a single file for a raw non-HF URL, or the
4518
+ * whole model (all split shards + the shared mmproj) for an HF repo file. Throws
4519
+ * DownloadError for caller-actionable failures (no dir set, bad URL, insufficient
4520
+ * disk). */
4521
+ async enqueue(input) {
4467
4522
  const dir = this.primaryDir();
4468
4523
  if (!dir) throw new DownloadError("no_model_dir", "Add a model folder in Settings before downloading.");
4469
4524
  let repo = (input.repo ?? "").trim();
4470
- const subdir = (input.subdir ?? "").trim();
4471
- let url;
4472
- let filename;
4525
+ const explicitSubdir = (input.subdir ?? "").trim();
4526
+ let rfilename = (input.rfilename ?? "").trim();
4527
+ let rev = "main";
4473
4528
  if (input.url) {
4474
4529
  const u = normalizeHfBlobUrl(input.url.trim());
4475
4530
  if (!/^https?:\/\//i.test(u)) throw new DownloadError("invalid_url", "URL must start with http:// or https://.");
4476
- const path = safePathname(u);
4477
- if (!subdir && !/\.gguf$/i.test(path) && !HF_BLOB_RE.test(u.split("?")[0])) {
4478
- throw new DownloadError("invalid_url", "URL must point to a .gguf file.");
4479
- }
4480
- filename = basename2(path);
4481
- if (!subdir && !/\.gguf$/i.test(filename)) throw new DownloadError("invalid_url", "Could not derive a .gguf filename from that URL.");
4482
- url = u;
4483
- repo = "";
4484
- } else {
4485
- const rfilename = (input.rfilename ?? "").trim();
4486
- if (!repo || !rfilename) throw new DownloadError("invalid_request", "repo and rfilename are required.");
4487
- if (!subdir && !/\.gguf$/i.test(rfilename)) throw new DownloadError("invalid_url", "The file must be a .gguf.");
4488
- filename = basename2(rfilename);
4489
- url = `https://huggingface.co/${repo}/resolve/main/${rfilename}`;
4490
- }
4491
- const total = input.size ?? 0;
4492
- const destDir = subdir ? join15(dir, subdir) : dir;
4493
- if (subdir) mkdirSync8(destDir, { recursive: true });
4494
- if (total > 0) this.assertDisk(dir, total);
4495
- const id = `dl-${Date.now().toString(36)}-${(this.nextSeq++).toString(36)}`;
4496
- const rec = {
4497
- id,
4498
- name: filename,
4499
- repo,
4500
- url,
4501
- dest: join15(destDir, filename),
4502
- total,
4503
- received: 0,
4504
- status: "queued",
4505
- error: null,
4506
- bytesPerSec: 0,
4507
- sha256: input.sha256,
4508
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
4509
- };
4510
- this.records.set(id, rec);
4531
+ const hf2 = parseHfResolveUrl(u);
4532
+ if (hf2) {
4533
+ repo = hf2.repo;
4534
+ rfilename = hf2.rfilename;
4535
+ rev = hf2.rev;
4536
+ } else {
4537
+ const path = safePathname(u);
4538
+ if (!explicitSubdir && !/\.gguf$/i.test(path)) throw new DownloadError("invalid_url", "URL must point to a .gguf file.");
4539
+ const filename = basename2(path);
4540
+ if (!explicitSubdir && !/\.gguf$/i.test(filename)) throw new DownloadError("invalid_url", "Could not derive a .gguf filename from that URL.");
4541
+ const destDir2 = explicitSubdir ? join15(dir, explicitSubdir) : dir;
4542
+ mkdirSync8(destDir2, { recursive: true });
4543
+ if ((input.size ?? 0) > 0) this.assertDisk(dir, input.size);
4544
+ const dest = join15(destDir2, filename);
4545
+ if (this.hasLiveDest(dest)) return [];
4546
+ return this.commit([{ name: filename, repo: "", url: u, dest, total: input.size ?? 0, sha256: input.sha256 }]);
4547
+ }
4548
+ }
4549
+ if (!repo.includes("/") || !rfilename) throw new DownloadError("invalid_request", "repo and rfilename are required.");
4550
+ if (!explicitSubdir && !/\.gguf$/i.test(rfilename)) throw new DownloadError("invalid_url", "The file must be a .gguf.");
4551
+ if (explicitSubdir || !this.expand) {
4552
+ const filename = basename2(rfilename);
4553
+ const destDir2 = join15(dir, explicitSubdir || repoSubdir(repo));
4554
+ mkdirSync8(destDir2, { recursive: true });
4555
+ const dest = join15(destDir2, filename);
4556
+ if (this.hasLiveDest(dest)) return [];
4557
+ if ((input.size ?? 0) > 0) this.assertDisk(dir, input.size);
4558
+ return this.commit([
4559
+ { name: filename, repo, url: hfResolveUrl(repo, rev, rfilename), dest, total: input.size ?? 0, sha256: input.sha256 }
4560
+ ]);
4561
+ }
4562
+ const { dir: modelDir, files } = await this.expand(repo, rfilename, rev);
4563
+ const destDir = join15(dir, repoSubdir(repo), modelDir);
4564
+ mkdirSync8(destDir, { recursive: true });
4565
+ const targets = files.map((f) => ({ f, dest: join15(destDir, basename2(f.rfilename)) })).filter(({ f, dest }) => {
4566
+ if (this.hasLiveDest(dest)) return false;
4567
+ if (f.mmproj && existsSync14(dest)) {
4568
+ if (f.size <= 0) return false;
4569
+ try {
4570
+ return statSync3(dest).size !== f.size;
4571
+ } catch {
4572
+ return true;
4573
+ }
4574
+ }
4575
+ return true;
4576
+ });
4577
+ if (targets.length === 0) return [];
4578
+ const totalBytes = targets.reduce((s, { f }) => s + (f.size || 0), 0);
4579
+ if (totalBytes > 0) this.assertDisk(dir, totalBytes);
4580
+ return this.commit(
4581
+ targets.map(({ f, dest }) => ({
4582
+ name: basename2(f.rfilename),
4583
+ repo,
4584
+ url: hfResolveUrl(repo, rev, f.rfilename),
4585
+ dest,
4586
+ total: f.size || 0,
4587
+ sha256: f.sha256
4588
+ }))
4589
+ );
4590
+ }
4591
+ /** True when a queued/downloading/paused record already targets this exact dest — the
4592
+ * guard against two concurrent writers corrupting one file. Terminal records
4593
+ * (done/cancelled/error) don't block a fresh attempt. */
4594
+ hasLiveDest(dest) {
4595
+ for (const r of this.records.values()) {
4596
+ if (r.dest === dest && (r.status === "queued" || r.status === "downloading" || r.status === "paused")) return true;
4597
+ }
4598
+ return false;
4599
+ }
4600
+ /** Materialise queued records from resolved targets, persist once, and kick the queue. */
4601
+ commit(targets) {
4602
+ const created = [];
4603
+ for (const t of targets) {
4604
+ const id = `dl-${Date.now().toString(36)}-${(this.nextSeq++).toString(36)}`;
4605
+ const rec = {
4606
+ id,
4607
+ name: t.name,
4608
+ repo: t.repo,
4609
+ url: t.url,
4610
+ dest: t.dest,
4611
+ total: t.total,
4612
+ received: 0,
4613
+ status: "queued",
4614
+ error: null,
4615
+ bytesPerSec: 0,
4616
+ sha256: t.sha256,
4617
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4618
+ };
4619
+ this.records.set(id, rec);
4620
+ created.push(rec);
4621
+ }
4511
4622
  this.persist();
4512
4623
  this.pump();
4513
- return rec;
4624
+ return created;
4514
4625
  }
4515
4626
  /** Cancel an in-flight or queued job: abort the stream, delete the .part, mark
4516
4627
  * cancelled. Keeps the record so the UI can show + clear it. */
@@ -4605,6 +4716,7 @@ var DownloadManager = class {
4605
4716
  }
4606
4717
  const clen = Number(res.headers.get("content-length") ?? 0);
4607
4718
  if (clen > 0) rec.total = resuming ? startAt + clen : clen;
4719
+ if (clen > 0) this.assertDisk(dirname5(part), clen);
4608
4720
  const hash = rec.sha256 ? createHash3("sha256") : null;
4609
4721
  const verifyHash = hash !== null && startAt === 0;
4610
4722
  let lastTick = Date.now();
@@ -4763,6 +4875,30 @@ function safePathname(u) {
4763
4875
  return "";
4764
4876
  }
4765
4877
  }
4878
+ function parseHfResolveUrl(u) {
4879
+ let parsed;
4880
+ try {
4881
+ parsed = new URL(u);
4882
+ } catch {
4883
+ return null;
4884
+ }
4885
+ if (parsed.hostname !== "huggingface.co") return null;
4886
+ const parts = parsed.pathname.split("/").filter(Boolean);
4887
+ const ri = parts.indexOf("resolve");
4888
+ if (ri < 2 || parts.length < ri + 3) return null;
4889
+ const repo = parts.slice(0, ri).join("/");
4890
+ const rev = decodeURIComponent(parts[ri + 1]);
4891
+ const rfilename = parts.slice(ri + 2).map((s) => decodeURIComponent(s)).join("/");
4892
+ if (!repo.includes("/") || !rev || !rfilename || !/\.gguf$/i.test(rfilename)) return null;
4893
+ return { repo, rev, rfilename };
4894
+ }
4895
+ function hfResolveUrl(repo, rev, rfilename) {
4896
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
4897
+ return `https://huggingface.co/${enc(repo)}/resolve/${encodeURIComponent(rev)}/${enc(rfilename)}`;
4898
+ }
4899
+ function repoSubdir(repo) {
4900
+ return repo.replace(/\\/g, "/").split("/").filter((s) => s && s !== "." && s !== "..").join("/");
4901
+ }
4766
4902
 
4767
4903
  // src/bench/bench.ts
4768
4904
  import { execFile as execFile7 } from "child_process";
@@ -8044,13 +8180,13 @@ import { Hono } from "hono";
8044
8180
  import { cors } from "hono/cors";
8045
8181
  import { existsSync as existsSync21, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
8046
8182
  import { fileURLToPath } from "url";
8047
- import { dirname as dirname8, join as join23, normalize as normalize2 } from "path";
8183
+ import { dirname as dirname9, join as join23, normalize as normalize2 } from "path";
8048
8184
  import { Agent, setGlobalDispatcher } from "undici";
8049
8185
 
8050
8186
  // src/api/routes.ts
8051
8187
  import { streamSSE as streamSSE2 } from "hono/streaming";
8052
8188
  import { existsSync as existsSync20, mkdirSync as mkdirSync12, readFileSync as readFileSync9, readdirSync as readdirSync7, realpathSync, rmSync as rmSync10, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
8053
- import { basename as basename3, dirname as dirname7, join as join22, resolve, sep } from "path";
8189
+ import { basename as basename3, dirname as dirname8, join as join22, resolve, sep } from "path";
8054
8190
 
8055
8191
  // src/comfyui/gate-template.ts
8056
8192
  var GATE_VERSION = 2;
@@ -8988,7 +9124,7 @@ async function autoTitle2(d, convId, prevMessages, assistantReply, target) {
8988
9124
  }
8989
9125
 
8990
9126
  // src/engines/scan.ts
8991
- import { dirname as dirname5 } from "path";
9127
+ import { dirname as dirname6 } from "path";
8992
9128
  import { existsSync as existsSync16, statSync as statSync4 } from "fs";
8993
9129
  var serverBinName = process.platform === "win32" ? "llama-server.exe" : "llama-server";
8994
9130
  function skipScanDir(name) {
@@ -9024,7 +9160,7 @@ var GENERIC_DIRS = /* @__PURE__ */ new Set([
9024
9160
  "osx"
9025
9161
  ]);
9026
9162
  function meaningfulFolder(binPath) {
9027
- const parts = dirname5(binPath).split(/[\\/]+/).filter(Boolean);
9163
+ const parts = dirname6(binPath).split(/[\\/]+/).filter(Boolean);
9028
9164
  for (let i = parts.length - 1; i >= 0; i--) {
9029
9165
  if (!GENERIC_DIRS.has(parts[i].toLowerCase())) return parts[i];
9030
9166
  }
@@ -9554,7 +9690,7 @@ async function checkBuildPrereqs(toolchainDirs = []) {
9554
9690
  // src/engines/build-runner.ts
9555
9691
  import { execFile as execFile9, spawn as spawn4 } from "child_process";
9556
9692
  import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync8, readdirSync as readdirSync5, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
9557
- import { delimiter as delimiter2, dirname as dirname6, join as join19 } from "path";
9693
+ import { delimiter as delimiter2, dirname as dirname7, join as join19 } from "path";
9558
9694
  import { promisify as promisify6 } from "util";
9559
9695
  var execFileP6 = promisify6(execFile9);
9560
9696
  function buildDirName(repoUrl, branch) {
@@ -9667,7 +9803,7 @@ function cudaSoSourceDirs(env) {
9667
9803
  }
9668
9804
  });
9669
9805
  if (!nvccDir) return [];
9670
- const toolkitRoot = dirname6(nvccDir);
9806
+ const toolkitRoot = dirname7(nvccDir);
9671
9807
  return [join19(toolkitRoot, "lib64"), join19(toolkitRoot, "lib"), join19(toolkitRoot, "targets", "x86_64-linux", "lib")].filter((d) => {
9672
9808
  try {
9673
9809
  return existsSync17(d);
@@ -9888,8 +10024,8 @@ async function runBuild(req, hooks, signal) {
9888
10024
  if (!binPath) {
9889
10025
  throw new Error("Build finished but no llama-server binary was found in the output \u2014 see the log above.");
9890
10026
  }
9891
- if (isWindows) copyCudaRuntimeDlls(env, dirname6(binPath), hooks.log);
9892
- else copyCudaRuntimeLibs(env, dirname6(binPath), hooks.log);
10027
+ if (isWindows) copyCudaRuntimeDlls(env, dirname7(binPath), hooks.log);
10028
+ else copyCudaRuntimeLibs(env, dirname7(binPath), hooks.log);
9893
10029
  return { binPath, commit, buildRoot };
9894
10030
  }
9895
10031
 
@@ -10916,7 +11052,7 @@ function registerApi(app2, d) {
10916
11052
  } catch {
10917
11053
  return err2(c, 400, "fs_read_failed", "Could not read that folder (permission denied or not a directory).");
10918
11054
  }
10919
- const parentDir = real === home ? null : dirname7(real);
11055
+ const parentDir = real === home ? null : dirname8(real);
10920
11056
  const parent = parentDir && isWithinHome(parentDir, home) ? parentDir : null;
10921
11057
  return c.json({ path: real, parent, entries });
10922
11058
  });
@@ -11484,8 +11620,8 @@ function registerApi(app2, d) {
11484
11620
  app2.post("/api/v1/downloads", async (c) => {
11485
11621
  const b = await body2(c);
11486
11622
  try {
11487
- const rec = d.downloads.enqueue(b);
11488
- return c.json(rec, 202);
11623
+ const recs = await d.downloads.enqueue(b);
11624
+ return c.json({ downloads: recs }, 202);
11489
11625
  } catch (e) {
11490
11626
  return dlErr(c, e);
11491
11627
  }
@@ -12525,7 +12661,7 @@ async function recordOpenAiStreamUsage(d, body4) {
12525
12661
 
12526
12662
  // src/server.ts
12527
12663
  setGlobalDispatcher(new Agent({ keepAliveMaxTimeout: 6e4, connections: 10 }));
12528
- var WEB_ROOT = join23(dirname8(fileURLToPath(import.meta.url)), "webdist");
12664
+ var WEB_ROOT = join23(dirname9(fileURLToPath(import.meta.url)), "webdist");
12529
12665
  function createApp(d) {
12530
12666
  const app2 = new Hono();
12531
12667
  app2.use(
@@ -12583,7 +12719,7 @@ function contentType(file) {
12583
12719
  // src/cli.ts
12584
12720
  var version = "0.1.1";
12585
12721
  try {
12586
- const pkgPath = join24(dirname9(fileURLToPath2(import.meta.url)), "..", "package.json");
12722
+ const pkgPath = join24(dirname10(fileURLToPath2(import.meta.url)), "..", "package.json");
12587
12723
  version = JSON.parse(readFileSync11(pkgPath, "utf8")).version ?? version;
12588
12724
  } catch {
12589
12725
  }
@@ -12623,7 +12759,7 @@ if (argv[0] === "launch") {
12623
12759
  const launchConfigPath = argValue("--config", defaultConfigPath());
12624
12760
  const explicitPort = Number(argValue("--port", "")) || void 0;
12625
12761
  const port2 = resolveDaemonPort(
12626
- dirname9(launchConfigPath),
12762
+ dirname10(launchConfigPath),
12627
12763
  explicitPort,
12628
12764
  configuredDaemonPort(launchConfigPath)
12629
12765
  );
@@ -12697,7 +12833,12 @@ void scanner.rescan();
12697
12833
  var hashes = new HashStore(store.dir());
12698
12834
  var db = new ConversationStore(store.dir());
12699
12835
  var hf = new HfClient(() => store.snapshot().hf.token, version);
12700
- var downloads = new DownloadManager(store, () => void scanner.rescan(), () => hf.authHeaders());
12836
+ var downloads = new DownloadManager(
12837
+ store,
12838
+ () => void scanner.rescan(),
12839
+ () => hf.authHeaders(),
12840
+ (repo, rfilename, rev) => hf.expandModelFiles(repo, rfilename, rev)
12841
+ );
12701
12842
  var bench = new BenchRunner(manager, store, scanner, registry, version, hf);
12702
12843
  var comfy = new ComfyGuard(store, manager);
12703
12844
  var modelRouter = new ModelRouter(store, registry, manager, scanner, comfy);
@@ -1,2 +1,2 @@
1
- import{j as e}from"./radix-9WP-OkMY.js";import{r as u}from"./vendor-W5GIuFcv.js";import{a as w,d as k,A as T,u as v,L as h,e as S,c as b,f as C,g as q,X as A}from"./index-K2ZQBdky.js";import{P as N}from"./plus-mSm32jpN.js";import{C as y}from"./circle-x-koFsrlGA.js";import{C as O}from"./circle-check-C4aACGyb.js";import{W as E}from"./wrench-Ceqe74n2.js";const _=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],D=w("circle-dot",_);const L=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 6v6l4 2",key:"mmk7yg"}]],P=w("clock",L),p={all:["agent-runs"],list:()=>[...p.all,"list"],detail:t=>[...p.all,"detail",t]};async function g(t,a){const l={Accept:"application/json",...k(),...a?.headers??{}},i=await fetch(t,{...a,headers:l});if(i.status===204)return;const r=await i.text(),o=r?JSON.parse(r):void 0;if(!i.ok){const d=o;throw new T(d?.error?.code??"http_error",d?.error?.message??`Request failed with status ${i.status}.`,i.status)}return o}async function $(){return g("/api/v1/agents/runs")}async function I(t){return g(`/api/v1/agents/runs/${t}`)}async function Q(t){return g("/api/v1/agents/runs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async function K(t){await g(`/api/v1/agents/runs/${t}`,{method:"DELETE"})}async function*F(t,a=0,l){const i=`/api/v1/agents/runs/${t}/stream?fromSeq=${a}`,r=await fetch(i,{headers:{Accept:"text/event-stream",...k()},signal:l});if(!r.ok||!r.body)throw new Error(`Stream error: ${r.status}`);const o=r.body.getReader(),d=new TextDecoder;let m="";try{for(;;){const{done:s,value:x}=await o.read();if(s)break;m+=d.decode(x,{stream:!0});const c=m.split(`
1
+ import{j as e}from"./radix-9WP-OkMY.js";import{r as u}from"./vendor-W5GIuFcv.js";import{a as w,d as k,A as T,u as v,L as h,e as S,c as b,f as C,g as q,X as A}from"./index-CjGc01NF.js";import{P as N}from"./plus-3YM8ko_h.js";import{C as y}from"./circle-x-CrulSiVz.js";import{C as O}from"./circle-check-C9nr7e7f.js";import{W as E}from"./wrench-2q903Ji-.js";const _=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],D=w("circle-dot",_);const L=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 6v6l4 2",key:"mmk7yg"}]],P=w("clock",L),p={all:["agent-runs"],list:()=>[...p.all,"list"],detail:t=>[...p.all,"detail",t]};async function g(t,a){const l={Accept:"application/json",...k(),...a?.headers??{}},i=await fetch(t,{...a,headers:l});if(i.status===204)return;const r=await i.text(),o=r?JSON.parse(r):void 0;if(!i.ok){const d=o;throw new T(d?.error?.code??"http_error",d?.error?.message??`Request failed with status ${i.status}.`,i.status)}return o}async function $(){return g("/api/v1/agents/runs")}async function I(t){return g(`/api/v1/agents/runs/${t}`)}async function Q(t){return g("/api/v1/agents/runs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async function K(t){await g(`/api/v1/agents/runs/${t}`,{method:"DELETE"})}async function*F(t,a=0,l){const i=`/api/v1/agents/runs/${t}/stream?fromSeq=${a}`,r=await fetch(i,{headers:{Accept:"text/event-stream",...k()},signal:l});if(!r.ok||!r.body)throw new Error(`Stream error: ${r.status}`);const o=r.body.getReader(),d=new TextDecoder;let m="";try{for(;;){const{done:s,value:x}=await o.read();if(s)break;m+=d.decode(x,{stream:!0});const c=m.split(`
2
2
  `);m=c.pop()??"";let n="message";for(const f of c){if(f.startsWith("event: ")){n=f.slice(7).trim();continue}if(f.startsWith("data: ")){const j=f.slice(6).trim();if(j==="[DONE]")return;try{const R=JSON.parse(j);if(yield{event:n,data:R},n==="done"||n==="error")return}catch{}n="message"}}}}finally{o.cancel()}}const J={queued:e.jsx(P,{size:12}),running:e.jsx(h,{size:12,className:"animate-spin"}),done:e.jsx(O,{size:12}),failed:e.jsx(y,{size:12}),cancelled:e.jsx(y,{size:12}),interrupted:e.jsx(y,{size:12})},M={queued:"text-muted",running:"text-accent",done:"text-green-500",failed:"text-red-500",cancelled:"text-muted",interrupted:"text-orange-500"};function z({status:t}){return e.jsxs("span",{className:b("flex items-center gap-1 text-[11px] capitalize",M[t]),children:[J[t],t]})}function W(t){const a=Date.now()-new Date(t).getTime();return a<6e4?"just now":a<36e5?`${Math.floor(a/6e4)}m ago`:a<864e5?`${Math.floor(a/36e5)}h ago`:new Date(t).toLocaleDateString()}const U=[{id:"web_search",label:"Web search",hint:"Search the web (needs a provider configured in Settings)"},{id:"fetch_url",label:"Fetch URL",hint:"Read the contents of a web page"},{id:"run_code",label:"Run code",hint:"Execute JS in a sandbox — no mid-run confirmation, opt in only if you trust the task"}];function B({onClose:t,onCreated:a}){const[l,i]=u.useState(""),[r,o]=u.useState(""),[d,m]=u.useState({web_search:!0,fetch_url:!0,run_code:!1}),s=C(),x=n=>m(f=>({...f,[n]:!f[n]})),c=q({mutationFn:()=>Q({title:l.slice(0,60)||"Agent run",systemPrompt:r||void 0,userMessage:l,allowedTools:Object.entries(d).filter(([,n])=>n).map(([n])=>n)}),onSuccess:n=>{s.invalidateQueries({queryKey:p.list()}),a(n.id),t()}});return e.jsx("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/40",onClick:t,children:e.jsxs("div",{className:"w-full max-w-lg rounded-xl border border-border bg-panel p-5 shadow-xl",onClick:n=>n.stopPropagation(),children:[e.jsxs("div",{className:"mb-4 flex items-center justify-between",children:[e.jsx("h2",{className:"text-[15px] font-semibold",children:"New agent run"}),e.jsx("button",{type:"button",onClick:t,className:"text-faint hover:text-ink",children:e.jsx(A,{size:16})})]}),e.jsx("label",{className:"mb-1 block text-[12px] text-muted",children:"Task"}),e.jsx("textarea",{autoFocus:!0,value:l,onChange:n=>i(n.target.value),placeholder:"Describe what the agent should do…",rows:4,className:"mb-3 w-full resize-none rounded-lg border border-border bg-input px-3 py-2 text-[13px] text-ink placeholder:text-faint focus:border-accent focus:outline-none"}),e.jsxs("label",{className:"mb-1 block text-[12px] text-muted",children:["System prompt ",e.jsx("span",{className:"text-faint",children:"(optional)"})]}),e.jsx("textarea",{value:r,onChange:n=>o(n.target.value),placeholder:"You are a helpful assistant…",rows:2,className:"mb-4 w-full resize-none rounded-lg border border-border bg-input px-3 py-2 text-[13px] text-ink placeholder:text-faint focus:border-accent focus:outline-none"}),e.jsx("label",{className:"mb-1.5 block text-[12px] text-muted",children:"Tools"}),e.jsx("div",{className:"mb-4 space-y-1.5",children:U.map(n=>e.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg border border-border px-2.5 py-2 hover:bg-panel-2",children:[e.jsx("input",{type:"checkbox",checked:d[n.id],onChange:()=>x(n.id),className:"mt-0.5 accent-[var(--accent)]"}),e.jsxs("span",{className:"min-w-0",children:[e.jsx("span",{className:"block text-[13px] font-medium text-ink",children:n.label}),e.jsx("span",{className:"block text-[11px] leading-snug text-faint",children:n.hint})]})]},n.id))}),e.jsxs("div",{className:"flex items-center justify-end gap-2",children:[e.jsx("button",{type:"button",onClick:t,className:"rounded-lg px-3 py-1.5 text-[13px] text-muted hover:text-ink",children:"Cancel"}),e.jsxs("button",{type:"button",disabled:!l.trim()||c.isPending,onClick:()=>c.mutate(),className:"flex items-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-[13px] font-medium text-white disabled:opacity-50",children:[c.isPending?e.jsx(h,{size:13,className:"animate-spin"}):e.jsx(S,{size:13}),"Run agent"]})]}),c.isError&&e.jsx("p",{className:"mt-2 text-[12px]",style:{color:"var(--err)"},children:c.error.message})]})})}function X({call:t}){const[a,l]=u.useState(!1),i=typeof t.args?.query=="string"?t.args.query:typeof t.args?.url=="string"?t.args.url:JSON.stringify(t.args??{});return e.jsxs("div",{className:"w-full max-w-[85%] rounded-lg border border-border bg-panel px-2.5 py-1.5 text-[12px]",children:[e.jsxs("button",{type:"button",onClick:()=>l(r=>!r),className:"flex w-full items-center gap-1.5 text-left text-muted hover:text-ink",children:[e.jsx(E,{size:12,className:"shrink-0 text-accent"}),e.jsx("span",{className:"font-medium text-ink",children:t.name}),e.jsx("span",{className:"truncate text-faint",children:i})]}),a&&e.jsx("pre",{className:"mt-1.5 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-panel-2 p-2 text-[11px] text-muted",children:t.result})]})}function H({run:t}){const a=C(),l=u.useRef(null),[i,r]=u.useState(""),o=u.useRef(null),d=q({mutationFn:()=>K(t.id),onSuccess:()=>{a.invalidateQueries({queryKey:p.list()})}});u.useEffect(()=>{if(t.status!=="running"&&t.status!=="queued")return;const s=new AbortController;l.current=s;let x="";return(async()=>{try{for await(const c of F(t.id,0,s.signal))if(c.event==="delta")x+=c.data.delta,r(x);else if(c.event==="done"||c.event==="error"){r(""),a.invalidateQueries({queryKey:p.detail(t.id)}),a.invalidateQueries({queryKey:p.list()});break}}catch{}})(),()=>{s.abort(),l.current=null}},[t.id,t.status,a]),u.useEffect(()=>{o.current?.scrollIntoView({behavior:"smooth"})},[i,t.messages?.length]);const m=t.messages??[];return e.jsxs("div",{className:"flex h-full flex-col",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-4 py-2.5",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-[14px] font-medium text-ink",children:t.title}),e.jsx(z,{status:t.status})]}),(t.status==="running"||t.status==="queued")&&e.jsx("button",{type:"button",onClick:()=>d.mutate(),className:"rounded-lg border border-border px-3 py-1 text-[12px] text-muted transition-colors hover:border-red-400 hover:text-red-500",children:d.isPending?e.jsx(h,{size:12,className:"inline animate-spin"}):"Cancel"})]}),e.jsxs("div",{className:"flex-1 space-y-3 overflow-y-auto p-4",children:[m.map(s=>e.jsxs("div",{className:b("flex flex-col gap-1.5",s.role==="user"?"items-end":"items-start"),children:[s.toolCalls?.map(x=>e.jsx(X,{call:x},x.id)),(s.content||s.role==="user"||t.status==="running")&&e.jsx("div",{className:b("max-w-[85%] rounded-xl px-3 py-2 text-[13px] leading-relaxed",s.role==="user"?"bg-accent/15 text-ink":"bg-panel-2 text-ink"),children:e.jsx("p",{className:"whitespace-pre-wrap break-words",children:s.content||(s.role==="assistant"&&t.status==="running"?"…":"")})})]},s.id)),i&&e.jsx("div",{className:"flex gap-2",children:e.jsx("div",{className:"max-w-[85%] rounded-xl bg-panel-2 px-3 py-2 text-[13px] leading-relaxed text-ink",children:e.jsx("p",{className:"whitespace-pre-wrap break-words",children:i})})}),t.error&&e.jsxs("div",{className:"rounded-lg bg-red-500/10 px-3 py-2 text-[12px] text-red-500",children:["Error: ",t.error]}),e.jsx("div",{ref:o})]})]})}function ne(){const[t,a]=u.useState(null),[l,i]=u.useState(!1),r=v({queryKey:p.list(),queryFn:$,refetchInterval:s=>(s.state.data??[]).some(n=>n.status==="queued"||n.status==="running")?3e3:!1}),o=v({queryKey:p.detail(t??""),queryFn:()=>I(t),enabled:!!t,refetchInterval:s=>{const x=s.state.data;return x?.status==="queued"||x?.status==="running"?5e3:!1}}),d=r.data??[],m=o.data;return e.jsxs("div",{className:"flex h-full",children:[e.jsxs("div",{className:"flex w-64 shrink-0 flex-col border-r border-border",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-border px-3 py-2.5",children:[e.jsx("span",{className:"text-[13px] font-semibold text-ink",children:"Agents"}),e.jsxs("button",{type:"button",title:"New agent run",onClick:()=>i(!0),className:"flex items-center gap-1 rounded-lg bg-accent px-2 py-1 text-[11px] font-medium text-white transition-opacity hover:opacity-90",children:[e.jsx(N,{size:11})," New"]})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[r.isLoading&&e.jsx("div",{className:"flex h-20 items-center justify-center",children:e.jsx(h,{size:16,className:"animate-spin text-faint"})}),d.length===0&&!r.isLoading&&e.jsxs("div",{className:"px-4 py-8 text-center",children:[e.jsx(S,{size:28,className:"mx-auto mb-2 text-faint"}),e.jsx("p",{className:"text-[12px] text-muted",children:"No agent runs yet."}),e.jsx("p",{className:"mt-0.5 text-[11px] text-faint",children:"Click “New” to start one."})]}),d.map(s=>e.jsxs("button",{type:"button",onClick:()=>a(s.id),className:b("w-full border-b border-border px-3 py-2.5 text-left transition-colors hover:bg-panel-2",t===s.id&&"bg-panel-2"),children:[e.jsx("p",{className:"mb-0.5 truncate text-[13px] font-medium text-ink",children:s.title}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx(z,{status:s.status}),e.jsx("span",{className:"text-[10px] text-faint",children:W(s.createdAt)})]})]},s.id))]})]}),e.jsx("div",{className:"flex min-w-0 flex-1 flex-col",children:t?o.isLoading?e.jsx("div",{className:"flex h-full items-center justify-center",children:e.jsx(h,{size:20,className:"animate-spin text-faint"})}):m?e.jsx(H,{run:m}):null:e.jsxs("div",{className:"flex h-full flex-col items-center justify-center text-center",children:[e.jsx(D,{size:32,className:"mb-3 text-faint"}),e.jsx("p",{className:"text-[14px] font-medium text-ink",children:"Select a run"}),e.jsx("p",{className:"mt-1 text-[12px] text-muted",children:"or create a new agent run to get started"}),e.jsxs("button",{type:"button",onClick:()=>i(!0),className:"mt-4 flex items-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-[13px] font-medium text-white hover:opacity-90",children:[e.jsx(N,{size:13})," New agent run"]})]})}),l&&e.jsx(B,{onClose:()=>i(!1),onCreated:s=>{a(s),r.refetch()}})]})}export{ne as AgentsScreen};