@quanta-intellect/vessel-browser 0.1.31 → 0.1.33

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 CHANGED
@@ -11,7 +11,7 @@
11
11
  </div>
12
12
 
13
13
 
14
- Open source chromium-based browser for persistent web agents on Linux (Mac/Windows support to come).
14
+ Open source chromium-based browser for persistent web agents. Linux is the most mature install target today, and macOS release packaging is available from source.
15
15
 
16
16
  Vessel gives external agent harnesses a real browser with durable state, MCP control, and a human-visible supervisory UI. It is built for long-running workflows where the agent drives and the human audits, intervenes, and redirects when needed.
17
17
 
@@ -200,12 +200,25 @@ npm run dist:dir
200
200
 
201
201
  # Package a Linux AppImage
202
202
  npm run dist
203
+
204
+ # Package an unpacked macOS app bundle (run on macOS)
205
+ npm run dist:mac:dir
206
+
207
+ # Package macOS DMG + ZIP artifacts (run on macOS)
208
+ npm run dist:mac
209
+
210
+ # Package signed macOS DMG + ZIP artifacts (run on macOS with signing set up)
211
+ npm run dist:mac:signed
203
212
  ```
204
213
 
205
214
  Notes:
206
215
 
207
216
  - `npm run dev` still launches the stock Electron binary, so Linux may continue showing the default Electron gear icon in development
208
217
  - packaged builds created with `npm run dist` / `npm run dist:dir` use the Vessel app icon
218
+ - `npm run build:icon:mac` regenerates `resources/vessel-icon.icns` from `resources/vessel-icon.png` for macOS packaging
219
+ - `npm run dist:mac` and `npm run dist:mac:dir` intentionally disable auto-signing so local packaging works on any Mac without keychain setup
220
+ - `npm run dist:mac:signed` and `npm run dist:mac:dir:signed` use normal `electron-builder` signing discovery; if your login keychain has duplicate Apple certs, clean those up or use a dedicated keychain before running the signed path
221
+ - signed builds are still not notarized by this repo out of the box, so Gatekeeper warnings remain until notarization is added for release publishing
209
222
  - the tracked smoke test runs typecheck, build, the MCP stdio proxy regression check, and the Electron navigation regression harness
210
223
  - for headless CI, run the smoke test under `xvfb-run -a npm run smoke:test`
211
224
 
package/out/main/index.js CHANGED
@@ -11509,6 +11509,325 @@ async function submitForm$1(wc, args) {
11509
11509
  async function clickElementBySelector(wc, selector) {
11510
11510
  return clickResolvedSelector$1(wc, selector);
11511
11511
  }
11512
+ const HUGGING_FACE_HUB_HOSTS = /* @__PURE__ */ new Set(["huggingface.co", "www.huggingface.co"]);
11513
+ const HUGGING_FACE_MODEL_TASKS = [
11514
+ {
11515
+ value: "automatic-speech-recognition",
11516
+ label: "automatic speech recognition",
11517
+ phrases: ["automatic speech recognition", "speech recognition", "asr"]
11518
+ },
11519
+ {
11520
+ value: "text-classification",
11521
+ label: "text classification",
11522
+ phrases: ["text classification", "classification"]
11523
+ },
11524
+ {
11525
+ value: "token-classification",
11526
+ label: "token classification",
11527
+ phrases: ["token classification", "named entity recognition", "ner"]
11528
+ },
11529
+ {
11530
+ value: "question-answering",
11531
+ label: "question answering",
11532
+ phrases: ["question answering", "qa"]
11533
+ },
11534
+ {
11535
+ value: "sentence-similarity",
11536
+ label: "sentence similarity",
11537
+ phrases: ["sentence similarity", "semantic similarity"]
11538
+ },
11539
+ {
11540
+ value: "feature-extraction",
11541
+ label: "feature extraction",
11542
+ phrases: ["feature extraction", "embeddings", "embedding"]
11543
+ },
11544
+ {
11545
+ value: "image-classification",
11546
+ label: "image classification",
11547
+ phrases: ["image classification"]
11548
+ },
11549
+ {
11550
+ value: "object-detection",
11551
+ label: "object detection",
11552
+ phrases: ["object detection"]
11553
+ },
11554
+ {
11555
+ value: "image-to-text",
11556
+ label: "image to text",
11557
+ phrases: ["image to text", "image-to-text", "ocr"]
11558
+ },
11559
+ {
11560
+ value: "text-to-image",
11561
+ label: "text to image",
11562
+ phrases: ["text to image", "text-to-image"]
11563
+ },
11564
+ {
11565
+ value: "text-to-speech",
11566
+ label: "text to speech",
11567
+ phrases: ["text to speech", "text-to-speech", "tts"]
11568
+ },
11569
+ {
11570
+ value: "text-generation",
11571
+ label: "text generation",
11572
+ phrases: [
11573
+ "text generation",
11574
+ "text-generation",
11575
+ "llm",
11576
+ "chat model",
11577
+ "chat models",
11578
+ "instruct model",
11579
+ "instruct models"
11580
+ ]
11581
+ },
11582
+ {
11583
+ value: "text2text-generation",
11584
+ label: "text2text generation",
11585
+ phrases: [
11586
+ "text2text generation",
11587
+ "text-to-text generation",
11588
+ "text to text generation"
11589
+ ]
11590
+ },
11591
+ {
11592
+ value: "summarization",
11593
+ label: "summarization",
11594
+ phrases: ["summarization", "summarize", "summarizer"]
11595
+ },
11596
+ {
11597
+ value: "translation",
11598
+ label: "translation",
11599
+ phrases: ["translation", "translate", "translator"]
11600
+ }
11601
+ ];
11602
+ const HUGGING_FACE_MODEL_LIBRARIES = [
11603
+ { value: "sentence-transformers", label: "sentence-transformers", phrases: ["sentence-transformers"] },
11604
+ { value: "transformers.js", label: "transformers.js", phrases: ["transformers.js"] },
11605
+ { value: "transformers", label: "transformers", phrases: ["transformers"] },
11606
+ { value: "diffusers", label: "diffusers", phrases: ["diffusers"] },
11607
+ { value: "safetensors", label: "safetensors", phrases: ["safetensors"] },
11608
+ { value: "pytorch", label: "pytorch", phrases: ["pytorch"] },
11609
+ { value: "tf", label: "tensorflow", phrases: ["tensorflow", "tf"] },
11610
+ { value: "jax", label: "jax", phrases: ["jax"] },
11611
+ { value: "onnx", label: "onnx", phrases: ["onnx"] },
11612
+ { value: "gguf", label: "gguf", phrases: ["gguf"] },
11613
+ { value: "mlx", label: "mlx", phrases: ["mlx"] }
11614
+ ];
11615
+ function escapeRegex(value) {
11616
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11617
+ }
11618
+ function stripSearchPhrase(query, phrase) {
11619
+ return query.replace(
11620
+ new RegExp(`(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`, "gi"),
11621
+ " "
11622
+ );
11623
+ }
11624
+ function collapseSearchTerms(query) {
11625
+ return query.replace(/\b(on|in|for|with|without|from|by|to|of|the|a|an|and|or)\b/gi, " ").replace(/\b(find|show|search|browse|look(?:ing)?(?:\s+for)?|need|want)\b/gi, " ").replace(/\bhugging\s*face\b/gi, " ").replace(/\s+/g, " ").trim();
11626
+ }
11627
+ function normalizeSearchQuery(query) {
11628
+ return query.replace(/\s+/g, " ").trim();
11629
+ }
11630
+ function extractFirstMatchingFilter(query, filters) {
11631
+ for (const filter of filters) {
11632
+ for (const phrase of filter.phrases) {
11633
+ const pattern = new RegExp(
11634
+ `(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`,
11635
+ "i"
11636
+ );
11637
+ if (!pattern.test(query)) continue;
11638
+ return {
11639
+ match: { value: filter.value, label: filter.label },
11640
+ remainingQuery: stripSearchPhrase(query, phrase)
11641
+ };
11642
+ }
11643
+ }
11644
+ return { match: null, remainingQuery: query };
11645
+ }
11646
+ function mapHuggingFaceParameterBucket(query) {
11647
+ const match = query.match(/\b(\d+(?:\.\d+)?)\s*(b|bn|billion|t|tn|trillion)\b/i);
11648
+ if (!match) {
11649
+ return { value: null, label: null, remainingQuery: query };
11650
+ }
11651
+ const amount = Number(match[1]);
11652
+ if (!Number.isFinite(amount) || amount <= 0) {
11653
+ return { value: null, label: null, remainingQuery: query };
11654
+ }
11655
+ const unit = match[2].toLowerCase();
11656
+ const billions = unit === "t" || unit === "tn" || unit === "trillion" ? amount * 1e3 : amount;
11657
+ let value;
11658
+ if (billions < 1) value = "n<1B";
11659
+ else if (billions < 3) value = "1B<n<3B";
11660
+ else if (billions < 6) value = "3B<n<6B";
11661
+ else if (billions < 9) value = "6B<n<9B";
11662
+ else if (billions < 12) value = "9B<n<12B";
11663
+ else if (billions < 24) value = "12B<n<24B";
11664
+ else if (billions < 32) value = "24B<n<32B";
11665
+ else if (billions < 64) value = "32B<n<64B";
11666
+ else if (billions < 128) value = "64B<n<128B";
11667
+ else if (billions < 256) value = "128B<n<256B";
11668
+ else if (billions < 500) value = "256B<n<500B";
11669
+ else if (billions < 1e3) value = "500B<n<1T";
11670
+ else value = "n>1T";
11671
+ return {
11672
+ value,
11673
+ label: `${amount}${unit.startsWith("t") ? "T" : "B"} size bucket`,
11674
+ remainingQuery: query.replace(match[0], " ")
11675
+ };
11676
+ }
11677
+ function chooseHuggingFaceSection(url, query) {
11678
+ const pathname = url.pathname.toLowerCase();
11679
+ const normalized = query.toLowerCase();
11680
+ const mentionsDatasets = /\b(dataset|datasets|corpus|benchmark|benchmarks)\b/.test(
11681
+ normalized
11682
+ );
11683
+ const mentionsSpaces = /\b(space|spaces|app|apps|demo|demos|gradio|streamlit)\b/.test(
11684
+ normalized
11685
+ );
11686
+ const mentionsModels = /\b(model|models|checkpoint|checkpoints|lora|loras|weights)\b/.test(
11687
+ normalized
11688
+ );
11689
+ if (mentionsDatasets && !mentionsModels && !mentionsSpaces) return "datasets";
11690
+ if (mentionsSpaces && !mentionsModels && !mentionsDatasets) return "spaces";
11691
+ if (mentionsModels && !mentionsDatasets && !mentionsSpaces) return "models";
11692
+ if (pathname.startsWith("/models")) return "models";
11693
+ if (pathname.startsWith("/datasets")) return "datasets";
11694
+ if (pathname.startsWith("/spaces")) return "spaces";
11695
+ if (HUGGING_FACE_MODEL_TASKS.some(
11696
+ (entry) => entry.phrases.some(
11697
+ (phrase) => new RegExp(`(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`, "i").test(
11698
+ normalized
11699
+ )
11700
+ )
11701
+ ) || HUGGING_FACE_MODEL_LIBRARIES.some(
11702
+ (entry) => entry.phrases.some(
11703
+ (phrase) => new RegExp(`(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`, "i").test(
11704
+ normalized
11705
+ )
11706
+ )
11707
+ ) || /\b\d+(?:\.\d+)?\s*(b|bn|billion|t|tn|trillion)\b/i.test(normalized)) {
11708
+ return "models";
11709
+ }
11710
+ return null;
11711
+ }
11712
+ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
11713
+ let url;
11714
+ try {
11715
+ url = new URL(currentUrl);
11716
+ } catch {
11717
+ return null;
11718
+ }
11719
+ const hostname = url.hostname.toLowerCase();
11720
+ if (!HUGGING_FACE_HUB_HOSTS.has(hostname)) {
11721
+ return null;
11722
+ }
11723
+ const query = rawQuery.trim();
11724
+ if (!query) return null;
11725
+ const section = chooseHuggingFaceSection(url, query);
11726
+ if (!section) return null;
11727
+ let remainingQuery = query;
11728
+ const target = new URL(`https://huggingface.co/${section}`);
11729
+ const appliedFilters = [];
11730
+ if (section === "models") {
11731
+ const taskResult = extractFirstMatchingFilter(
11732
+ remainingQuery,
11733
+ HUGGING_FACE_MODEL_TASKS
11734
+ );
11735
+ remainingQuery = taskResult.remainingQuery;
11736
+ if (taskResult.match) {
11737
+ target.searchParams.append("pipeline_tag", taskResult.match.value);
11738
+ appliedFilters.push(`task: ${taskResult.match.label}`);
11739
+ }
11740
+ const libraryResult = extractFirstMatchingFilter(
11741
+ remainingQuery,
11742
+ HUGGING_FACE_MODEL_LIBRARIES
11743
+ );
11744
+ remainingQuery = libraryResult.remainingQuery;
11745
+ if (libraryResult.match) {
11746
+ target.searchParams.append("library", libraryResult.match.value);
11747
+ appliedFilters.push(`library: ${libraryResult.match.label}`);
11748
+ }
11749
+ const parameterResult = mapHuggingFaceParameterBucket(remainingQuery);
11750
+ remainingQuery = parameterResult.remainingQuery;
11751
+ if (parameterResult.value && parameterResult.label) {
11752
+ target.searchParams.append("num_parameters", parameterResult.value);
11753
+ appliedFilters.push(parameterResult.label);
11754
+ }
11755
+ }
11756
+ remainingQuery = collapseSearchTerms(
11757
+ remainingQuery.replace(/\bmodels?\b/gi, " ").replace(/\bdatasets?\b/gi, " ").replace(/\bspaces?\b/gi, " ").replace(/\bapps?\b/gi, " ")
11758
+ );
11759
+ if (remainingQuery) {
11760
+ target.searchParams.set("search", remainingQuery);
11761
+ }
11762
+ if (!remainingQuery && appliedFilters.length === 0) {
11763
+ return null;
11764
+ }
11765
+ if (section === "spaces" && remainingQuery) {
11766
+ target.searchParams.set("includeNonRunning", "true");
11767
+ }
11768
+ return {
11769
+ url: target.toString(),
11770
+ source: "Hugging Face",
11771
+ section,
11772
+ appliedFilters
11773
+ };
11774
+ }
11775
+ const COMMON_SEARCH_QUERY_PARAMS = [
11776
+ "search",
11777
+ "q",
11778
+ "query",
11779
+ "keyword",
11780
+ "keywords",
11781
+ "term",
11782
+ "text"
11783
+ ];
11784
+ const COMMON_PAGINATION_PARAMS = [
11785
+ "p",
11786
+ "page",
11787
+ "offset",
11788
+ "start",
11789
+ "cursor",
11790
+ "skip"
11791
+ ];
11792
+ function looksLikeSearchResultsPath(pathname) {
11793
+ return /\/(search|results|browse|discover|find)(\/|$)/i.test(pathname);
11794
+ }
11795
+ function buildCommonSearchUrlShortcut(currentUrl, rawQuery) {
11796
+ let url;
11797
+ try {
11798
+ url = new URL(currentUrl);
11799
+ } catch {
11800
+ return null;
11801
+ }
11802
+ if (!/^https?:$/i.test(url.protocol)) {
11803
+ return null;
11804
+ }
11805
+ const query = normalizeSearchQuery(rawQuery);
11806
+ if (!query) return null;
11807
+ const existingParam = COMMON_SEARCH_QUERY_PARAMS.find(
11808
+ (param) => url.searchParams.has(param)
11809
+ );
11810
+ if (!existingParam && !looksLikeSearchResultsPath(url.pathname)) {
11811
+ return null;
11812
+ }
11813
+ const target = new URL(url.toString());
11814
+ const searchParam = existingParam ?? "q";
11815
+ target.searchParams.set(searchParam, query);
11816
+ for (const param of COMMON_PAGINATION_PARAMS) {
11817
+ target.searchParams.delete(param);
11818
+ }
11819
+ if (target.toString() === url.toString()) {
11820
+ return null;
11821
+ }
11822
+ return {
11823
+ url: target.toString(),
11824
+ source: "page URL",
11825
+ appliedFilters: existingParam ? [`updated ${existingParam} query`] : []
11826
+ };
11827
+ }
11828
+ function buildSearchShortcut(currentUrl, rawQuery) {
11829
+ return buildHuggingFaceSearchShortcut(currentUrl, rawQuery) ?? buildCommonSearchUrlShortcut(currentUrl, rawQuery);
11830
+ }
11512
11831
  async function locateSearchTarget(wc, explicitSelector) {
11513
11832
  if (explicitSelector) {
11514
11833
  return { selector: explicitSelector, submitSelector: null };
@@ -11775,6 +12094,19 @@ async function searchPage(wc, args) {
11775
12094
  if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
11776
12095
  return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
11777
12096
  }
12097
+ if (typeof args.selector !== "string") {
12098
+ const shortcut = buildSearchShortcut(wc.getURL(), query);
12099
+ if (shortcut) {
12100
+ const beforeUrl2 = wc.getURL();
12101
+ assertSafeURL(shortcut.url);
12102
+ wc.loadURL(shortcut.url);
12103
+ await waitForPotentialNavigation$1(wc, beforeUrl2, 4e3);
12104
+ const afterUrl2 = wc.getURL();
12105
+ const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
12106
+ const destination = shortcut.section ? ` ${shortcut.section}` : "";
12107
+ return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}`;
12108
+ }
12109
+ }
11778
12110
  const searchInfo = await locateSearchTarget(
11779
12111
  wc,
11780
12112
  typeof args.selector === "string" ? args.selector : void 0
@@ -18755,11 +19087,21 @@ function stopMcpServer() {
18755
19087
  });
18756
19088
  });
18757
19089
  }
19090
+ const VALID_KIT_CATEGORIES = /* @__PURE__ */ new Set([
19091
+ "research",
19092
+ "shopping",
19093
+ "productivity",
19094
+ "forms"
19095
+ ]);
18758
19096
  const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set([
18759
19097
  "research-collect",
18760
19098
  "price-scout",
18761
19099
  "form-filler"
18762
19100
  ]);
19101
+ const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
19102
+ function isSafeAutomationKitId(id) {
19103
+ return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
19104
+ }
18763
19105
  function getUserKitsDir() {
18764
19106
  return path$1.join(electron.app.getPath("userData"), "kits");
18765
19107
  }
@@ -18769,10 +19111,16 @@ function ensureKitsDir() {
18769
19111
  fs$1.mkdirSync(dir, { recursive: true });
18770
19112
  }
18771
19113
  }
19114
+ function getKitFilePath(id) {
19115
+ if (!isSafeAutomationKitId(id)) return null;
19116
+ const kitsDir = path$1.resolve(getUserKitsDir());
19117
+ const target = path$1.resolve(kitsDir, `${id}.kit.json`);
19118
+ return target.startsWith(`${kitsDir}${path$1.sep}`) ? target : null;
19119
+ }
18772
19120
  function isValidKit(value) {
18773
19121
  if (!value || typeof value !== "object") return false;
18774
19122
  const k = value;
18775
- return typeof k.id === "string" && k.id.length > 0 && typeof k.name === "string" && k.name.length > 0 && typeof k.description === "string" && typeof k.icon === "string" && typeof k.promptTemplate === "string" && k.promptTemplate.length > 0 && Array.isArray(k.inputs);
19123
+ return typeof k.id === "string" && isSafeAutomationKitId(k.id) && typeof k.name === "string" && k.name.length > 0 && typeof k.description === "string" && typeof k.category === "string" && VALID_KIT_CATEGORIES.has(k.category) && typeof k.icon === "string" && typeof k.promptTemplate === "string" && k.promptTemplate.length > 0 && Array.isArray(k.inputs);
18776
19124
  }
18777
19125
  function getInstalledKits() {
18778
19126
  ensureKitsDir();
@@ -18833,7 +19181,10 @@ async function installKitFromFile() {
18833
19181
  };
18834
19182
  }
18835
19183
  ensureKitsDir();
18836
- const dest = path$1.join(getUserKitsDir(), `${parsed.id}.kit.json`);
19184
+ const dest = getKitFilePath(parsed.id);
19185
+ if (!dest) {
19186
+ return { ok: false, error: "Kit id contains unsupported characters." };
19187
+ }
18837
19188
  try {
18838
19189
  fs$1.writeFileSync(dest, JSON.stringify(parsed, null, 2), "utf-8");
18839
19190
  } catch {
@@ -18841,12 +19192,21 @@ async function installKitFromFile() {
18841
19192
  }
18842
19193
  return { ok: true, kit: parsed };
18843
19194
  }
18844
- function uninstallKit(id) {
19195
+ function uninstallKit(id, scheduledKitIds) {
18845
19196
  if (BUNDLED_KIT_IDS.has(id)) {
18846
19197
  return { ok: false, error: "Built-in kits cannot be removed." };
18847
19198
  }
19199
+ if (scheduledKitIds?.has(id)) {
19200
+ return {
19201
+ ok: false,
19202
+ error: "This kit has active scheduled jobs. Delete or reassign them first."
19203
+ };
19204
+ }
18848
19205
  ensureKitsDir();
18849
- const target = path$1.join(getUserKitsDir(), `${id}.kit.json`);
19206
+ const target = getKitFilePath(id);
19207
+ if (!target) {
19208
+ return { ok: false, error: "Kit id contains unsupported characters." };
19209
+ }
18850
19210
  if (!fs$1.existsSync(target)) {
18851
19211
  return { ok: false, error: "Kit not found." };
18852
19212
  }
@@ -18860,6 +19220,9 @@ function uninstallKit(id) {
18860
19220
  let jobs = [];
18861
19221
  let removeIdleListener = null;
18862
19222
  let broadcastFn = null;
19223
+ function getScheduledKitIds() {
19224
+ return new Set(jobs.filter((j) => j.enabled).map((j) => j.kitId));
19225
+ }
18863
19226
  function getJobsPath() {
18864
19227
  return path$1.join(electron.app.getPath("userData"), "scheduled-jobs.json");
18865
19228
  }
@@ -19029,29 +19392,40 @@ async function fireJob(job, windowState, runtime2) {
19029
19392
  }
19030
19393
  function tick(windowState, runtime2) {
19031
19394
  if (isAIStreamActive()) return;
19032
- const now = /* @__PURE__ */ new Date();
19033
- let changed = false;
19034
- for (const job of jobs) {
19035
- if (!job.enabled) continue;
19036
- if (now < new Date(job.nextRunAt)) continue;
19037
- if (!tryBeginAIStream("scheduled")) break;
19038
- void fireJob(job, windowState, runtime2).finally(() => {
19395
+ const dueIds = jobs.filter((job) => job.enabled && /* @__PURE__ */ new Date() >= new Date(job.nextRunAt)).map((job) => job.id);
19396
+ if (dueIds.length === 0) return;
19397
+ if (!tryBeginAIStream("scheduled")) return;
19398
+ let idx = 0;
19399
+ const fireNext = () => {
19400
+ if (idx >= dueIds.length) {
19039
19401
  endAIStream("scheduled");
19040
19402
  queueMicrotask(() => tick(windowState, runtime2));
19041
- });
19042
- job.lastRunAt = now.toISOString();
19403
+ return;
19404
+ }
19405
+ const jobId = dueIds[idx++];
19406
+ const job = jobs.find((candidate) => candidate.id === jobId);
19407
+ if (!job || !job.enabled) {
19408
+ fireNext();
19409
+ return;
19410
+ }
19411
+ const firedAt = /* @__PURE__ */ new Date();
19412
+ if (firedAt < new Date(job.nextRunAt)) {
19413
+ fireNext();
19414
+ return;
19415
+ }
19416
+ job.lastRunAt = firedAt.toISOString();
19043
19417
  if (job.schedule.type === "once") {
19044
19418
  job.enabled = false;
19045
19419
  } else {
19046
- job.nextRunAt = computeNextRun(job.schedule, now).toISOString();
19420
+ job.nextRunAt = computeNextRun(job.schedule, firedAt).toISOString();
19047
19421
  }
19048
- changed = true;
19049
- break;
19050
- }
19051
- if (changed) {
19052
19422
  saveJobs();
19053
19423
  broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
19054
- }
19424
+ void fireJob(job, windowState, runtime2).catch((err) => {
19425
+ console.warn("[scheduler] Unexpected error firing job:", err);
19426
+ }).finally(fireNext);
19427
+ };
19428
+ fireNext();
19055
19429
  }
19056
19430
  function registerScheduleHandlers(windowState, runtime2, sendToAll) {
19057
19431
  broadcastFn = sendToAll;
@@ -19314,7 +19688,7 @@ function registerIpcHandlers(windowState, runtime2) {
19314
19688
  Channels.AI_STREAM_CHUNK,
19315
19689
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
19316
19690
  );
19317
- sendToRendererViews(Channels.AI_STREAM_END);
19691
+ sendToRendererViews(Channels.AI_STREAM_END, "failed");
19318
19692
  return { accepted: true };
19319
19693
  }
19320
19694
  if (!tryBeginAIStream("manual")) {
@@ -19331,7 +19705,7 @@ function registerIpcHandlers(windowState, runtime2) {
19331
19705
  activeChatProvider,
19332
19706
  activeTab?.view.webContents,
19333
19707
  (chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
19334
- () => sendToRendererViews(Channels.AI_STREAM_END),
19708
+ () => sendToRendererViews(Channels.AI_STREAM_END, "completed"),
19335
19709
  tabManager,
19336
19710
  runtime2,
19337
19711
  history
@@ -19340,7 +19714,7 @@ function registerIpcHandlers(windowState, runtime2) {
19340
19714
  const msg = err instanceof Error ? err.message : "Unknown error";
19341
19715
  sendToRendererViews(Channels.AI_STREAM_CHUNK, `
19342
19716
  [Error: ${msg}]`);
19343
- sendToRendererViews(Channels.AI_STREAM_END);
19717
+ sendToRendererViews(Channels.AI_STREAM_END, "failed");
19344
19718
  } finally {
19345
19719
  activeChatProvider = null;
19346
19720
  endAIStream("manual");
@@ -19762,7 +20136,7 @@ function registerIpcHandlers(windowState, runtime2) {
19762
20136
  });
19763
20137
  electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (_event, id) => {
19764
20138
  assertString(id, "id");
19765
- return uninstallKit(id);
20139
+ return uninstallKit(id, getScheduledKitIds());
19766
20140
  });
19767
20141
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
19768
20142
  }
@@ -137,7 +137,7 @@ const api = {
137
137
  return () => electron.ipcRenderer.removeListener(Channels.AI_STREAM_CHUNK, handler);
138
138
  },
139
139
  onStreamEnd: (cb) => {
140
- const handler = () => cb();
140
+ const handler = (_, status = "completed") => cb(status);
141
141
  electron.ipcRenderer.on(Channels.AI_STREAM_END, handler);
142
142
  return () => electron.ipcRenderer.removeListener(Channels.AI_STREAM_END, handler);
143
143
  },
@@ -2324,11 +2324,14 @@ const [hasFirstChunk, setHasFirstChunk] = createSignal(false);
2324
2324
  const [streamStartedAt, setStreamStartedAt] = createSignal(null);
2325
2325
  const [recentQueries, setRecentQueries] = createSignal([]);
2326
2326
  const [pendingQueries, setPendingQueries] = createSignal([]);
2327
+ const [pendingQueryActivities, setPendingQueryActivities] = createSignal([]);
2327
2328
  const [queueNotice, setQueueNotice] = createSignal(null);
2328
2329
  const [automationActivities, setAutomationActivities] = createSignal([]);
2329
2330
  let initialized$1 = false;
2330
2331
  let pendingDrainScheduled = false;
2331
2332
  let listenerCleanups = [];
2333
+ let pendingAutomationActivity = null;
2334
+ let activeAutomationActivityId = null;
2332
2335
  function trimMessages(next) {
2333
2336
  return next.length > MAX_MESSAGE_HISTORY ? next.slice(-MAX_MESSAGE_HISTORY) : next;
2334
2337
  }
@@ -2348,17 +2351,23 @@ async function dispatchQuery(prompt) {
2348
2351
  const result = await window.vessel.ai.query(prompt, buildHistory());
2349
2352
  return result.accepted;
2350
2353
  }
2351
- async function dispatchQueuedPrompt(prompt) {
2354
+ async function dispatchQueuedPrompt(prompt, activity) {
2355
+ pendingAutomationActivity = activity;
2352
2356
  const accepted = await dispatchQuery(prompt);
2353
2357
  if (!accepted) {
2358
+ pendingAutomationActivity = null;
2354
2359
  const queued = enqueuePendingPrompt(pendingQueries(), prompt, { atFront: true });
2355
2360
  setPendingQueries(queued.queue);
2356
2361
  setQueueNotice(queued.notice);
2362
+ if (queued.status === "queued") {
2363
+ setPendingQueryActivities((prev) => [activity, ...prev]);
2364
+ }
2357
2365
  }
2358
2366
  }
2359
2367
  function schedulePendingDrain() {
2360
2368
  if (pendingDrainScheduled || isStreaming()) return;
2361
2369
  if (pendingQueries().length === 0) {
2370
+ setPendingQueryActivities([]);
2362
2371
  setQueueNotice(null);
2363
2372
  return;
2364
2373
  }
@@ -2367,10 +2376,12 @@ function schedulePendingDrain() {
2367
2376
  pendingDrainScheduled = false;
2368
2377
  if (isStreaming()) return;
2369
2378
  const next = dequeuePendingPrompt(pendingQueries());
2379
+ const [nextActivity = null, ...remainingActivities] = pendingQueryActivities();
2370
2380
  setPendingQueries(next.queue);
2381
+ setPendingQueryActivities(remainingActivities);
2371
2382
  setQueueNotice(next.notice);
2372
2383
  if (next.nextPrompt) {
2373
- void dispatchQueuedPrompt(next.nextPrompt);
2384
+ void dispatchQueuedPrompt(next.nextPrompt, nextActivity);
2374
2385
  }
2375
2386
  });
2376
2387
  }
@@ -2386,14 +2397,35 @@ function init$1() {
2386
2397
  setIsStreaming(true);
2387
2398
  setHasFirstChunk(false);
2388
2399
  setStreamStartedAt(Date.now());
2400
+ if (pendingAutomationActivity) {
2401
+ const activity = pendingAutomationActivity;
2402
+ activeAutomationActivityId = activity.id;
2403
+ setAutomationActivities(
2404
+ (prev) => startAutomationActivity(prev, {
2405
+ id: activity.id,
2406
+ source: "scheduled",
2407
+ title: activity.title,
2408
+ icon: activity.icon,
2409
+ status: "running",
2410
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2411
+ })
2412
+ );
2413
+ pendingAutomationActivity = null;
2414
+ }
2389
2415
  }));
2390
2416
  listenerCleanups.push(window.vessel.ai.onStreamChunk((chunk) => {
2391
2417
  if (!hasFirstChunk()) {
2392
2418
  setHasFirstChunk(true);
2393
2419
  }
2394
2420
  setStreamingText((prev) => prev + chunk);
2421
+ if (activeAutomationActivityId) {
2422
+ const activityId = activeAutomationActivityId;
2423
+ setAutomationActivities(
2424
+ (prev) => appendAutomationActivityChunk(prev, activityId, chunk)
2425
+ );
2426
+ }
2395
2427
  }));
2396
- listenerCleanups.push(window.vessel.ai.onStreamEnd(() => {
2428
+ listenerCleanups.push(window.vessel.ai.onStreamEnd((status) => {
2397
2429
  const finalText = streamingText();
2398
2430
  if (finalText) {
2399
2431
  setMessages((prev) => {
@@ -2401,6 +2433,19 @@ function init$1() {
2401
2433
  return trimMessages(next);
2402
2434
  });
2403
2435
  }
2436
+ if (activeAutomationActivityId) {
2437
+ const activityId = activeAutomationActivityId;
2438
+ setAutomationActivities(
2439
+ (prev) => finishAutomationActivity(
2440
+ prev,
2441
+ activityId,
2442
+ status,
2443
+ (/* @__PURE__ */ new Date()).toISOString()
2444
+ )
2445
+ );
2446
+ activeAutomationActivityId = null;
2447
+ }
2448
+ pendingAutomationActivity = null;
2404
2449
  setStreamingText("");
2405
2450
  setIsStreaming(false);
2406
2451
  setHasFirstChunk(false);
@@ -2426,6 +2471,32 @@ function init$1() {
2426
2471
  }
2427
2472
  function useAI() {
2428
2473
  init$1();
2474
+ const query = async (prompt, activity = null) => {
2475
+ recordRecentQuery(prompt);
2476
+ if (isStreaming()) {
2477
+ const queued = enqueuePendingPrompt(pendingQueries(), prompt);
2478
+ setPendingQueries(queued.queue);
2479
+ setQueueNotice(queued.notice);
2480
+ if (queued.status === "queued") {
2481
+ setPendingQueryActivities((prev) => [...prev, activity]);
2482
+ }
2483
+ return queued.status;
2484
+ }
2485
+ setQueueNotice(null);
2486
+ pendingAutomationActivity = activity;
2487
+ const accepted = await dispatchQuery(prompt);
2488
+ if (!accepted) {
2489
+ pendingAutomationActivity = null;
2490
+ const queued = enqueuePendingPrompt(pendingQueries(), prompt, { atFront: true });
2491
+ setPendingQueries(queued.queue);
2492
+ setQueueNotice(queued.notice);
2493
+ if (queued.status === "queued") {
2494
+ setPendingQueryActivities((prev) => [activity, ...prev]);
2495
+ }
2496
+ return queued.status;
2497
+ }
2498
+ return "started";
2499
+ };
2429
2500
  return {
2430
2501
  messages,
2431
2502
  streamingText,
@@ -2438,39 +2509,28 @@ function useAI() {
2438
2509
  pendingQueryCount: () => pendingQueries().length,
2439
2510
  pendingQueryLimit: MAX_PENDING_QUERIES,
2440
2511
  queueNotice,
2441
- query: async (prompt) => {
2442
- recordRecentQuery(prompt);
2443
- if (isStreaming()) {
2444
- const queued = enqueuePendingPrompt(pendingQueries(), prompt);
2445
- setPendingQueries(queued.queue);
2446
- setQueueNotice(queued.notice);
2447
- return queued.status;
2448
- }
2449
- setQueueNotice(null);
2450
- const accepted = await dispatchQuery(prompt);
2451
- if (!accepted) {
2452
- const queued = enqueuePendingPrompt(pendingQueries(), prompt, { atFront: true });
2453
- setPendingQueries(queued.queue);
2454
- setQueueNotice(queued.notice);
2455
- return queued.status;
2456
- }
2457
- return "started";
2458
- },
2512
+ query,
2513
+ runAutomationPrompt: async (prompt, activity) => query(prompt, activity),
2459
2514
  cancel: () => window.vessel.ai.cancel(),
2460
2515
  removePendingQuery: (index) => {
2461
2516
  const next = removePendingPrompt(pendingQueries(), index);
2462
2517
  setPendingQueries(next.queue);
2518
+ setPendingQueryActivities(
2519
+ (prev) => prev.filter((_, itemIndex) => itemIndex !== index)
2520
+ );
2463
2521
  setQueueNotice(next.notice);
2464
2522
  },
2465
2523
  clearPendingQueries: () => {
2466
2524
  const next = clearPendingPromptQueue();
2467
2525
  setPendingQueries(next.queue);
2526
+ setPendingQueryActivities([]);
2468
2527
  setQueueNotice(next.notice);
2469
2528
  },
2470
2529
  clearHistory: () => {
2471
2530
  setMessages([]);
2472
2531
  const next = clearPendingPromptQueue();
2473
2532
  setPendingQueries(next.queue);
2533
+ setPendingQueryActivities([]);
2474
2534
  setQueueNotice(next.notice);
2475
2535
  }
2476
2536
  };
@@ -4440,6 +4500,11 @@ var Zap = (props) => createComponent(Icon_default, mergeProps(props, {
4440
4500
  name: "zap"
4441
4501
  }));
4442
4502
  var zap_default = Zap;
4503
+ const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set([
4504
+ "research-collect",
4505
+ "price-scout",
4506
+ "form-filler"
4507
+ ]);
4443
4508
  const BUNDLED_KITS = [
4444
4509
  {
4445
4510
  id: "research-collect",
@@ -4571,6 +4636,13 @@ Steps:
4571
4636
  }
4572
4637
  ];
4573
4638
  function renderKitPrompt(kit, values) {
4639
+ for (const input of kit.inputs) {
4640
+ if (input.required && !values[input.key]?.trim()) {
4641
+ console.warn(
4642
+ `[automation-kits] Required field "${input.key}" is empty for kit "${kit.id}".`
4643
+ );
4644
+ }
4645
+ }
4574
4646
  return kit.promptTemplate.replace(
4575
4647
  /\{\{(\w+)\}\}/g,
4576
4648
  (_, key) => values[key] ?? ""
@@ -4599,7 +4671,6 @@ const KitIcon = (props) => {
4599
4671
  }
4600
4672
  });
4601
4673
  };
4602
- const BUNDLED_KIT_IDS = new Set(BUNDLED_KITS.map((k) => k.id));
4603
4674
  const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
4604
4675
  function formatScheduleLabel(job) {
4605
4676
  const {
@@ -4650,7 +4721,7 @@ function toLocalDateTimeInput(iso) {
4650
4721
  }
4651
4722
  const AutomationTab = (props) => {
4652
4723
  const {
4653
- query,
4724
+ runAutomationPrompt,
4654
4725
  isStreaming: isStreaming2,
4655
4726
  automationActivities: automationActivities2
4656
4727
  } = useAI();
@@ -4739,9 +4810,15 @@ const AutomationTab = (props) => {
4739
4810
  const kit = selectedKit();
4740
4811
  if (!kit || !canRun()) return;
4741
4812
  const prompt = renderKitPrompt(kit, fieldValues());
4813
+ const activityId = `adhoc:${kit.id}:${Date.now()}`;
4814
+ const result = await runAutomationPrompt(prompt, {
4815
+ id: activityId,
4816
+ title: kit.name,
4817
+ icon: kit.icon
4818
+ });
4819
+ if (result === "rejected") return;
4742
4820
  setSelectedKit(null);
4743
4821
  props.onRun();
4744
- await query(prompt);
4745
4822
  };
4746
4823
  const handleSchedule = async () => {
4747
4824
  const kit = selectedKit();
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:;" />
7
7
  <title>Vessel</title>
8
- <script type="module" crossorigin src="./assets/index-DVD9XuhC.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-BFdOm6Op.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="./assets/index-eS3ccAls.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@quanta-intellect/vessel-browser",
3
3
  "mcpName": "io.github.unmodeled-tyler/vessel-browser",
4
- "version": "0.1.31",
5
- "description": "AI-native web browser for Linux — persistent browser runtime for autonomous agents with human supervision",
4
+ "version": "0.1.33",
5
+ "description": "AI-native web browser runtime for autonomous agents with human supervision",
6
6
  "main": "./out/main/index.js",
7
7
  "bin": {
8
8
  "vessel-browser": "./bin/vessel-browser.js"
@@ -18,8 +18,13 @@
18
18
  "scripts": {
19
19
  "dev": "ELECTRON_DISABLE_SANDBOX=1 electron-vite dev",
20
20
  "build": "electron-vite build",
21
+ "build:icon:mac": "scripts/build-macos-icon.sh",
21
22
  "dist": "npm run build && electron-builder --linux --publish never",
22
23
  "dist:dir": "npm run build && electron-builder --linux dir --publish never",
24
+ "dist:mac": "npm run build:icon:mac && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dmg zip --publish never",
25
+ "dist:mac:dir": "npm run build:icon:mac && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dir --publish never",
26
+ "dist:mac:signed": "npm run build:icon:mac && npm run build && electron-builder --mac dmg zip --publish never",
27
+ "dist:mac:dir:signed": "npm run build:icon:mac && npm run build && electron-builder --mac dir --publish never",
23
28
  "preview": "electron-vite preview",
24
29
  "typecheck": "tsc --noEmit",
25
30
  "prepublishOnly": "npm run build",