@alfe.ai/openclaw-chat 0.7.0 → 0.8.1

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/plugin.d.cts CHANGED
@@ -21,6 +21,10 @@
21
21
  * DEVELOPING.md → "Publishing public @alfe.ai/* packages"). Keep in sync with
22
22
  * the transport union.
23
23
  */
24
+ interface StoredSelectOption {
25
+ label: string;
26
+ value: string;
27
+ }
24
28
  type StoredMessageComponent = {
25
29
  type: 'link_button';
26
30
  id: string;
@@ -34,6 +38,31 @@ type StoredMessageComponent = {
34
38
  label: string;
35
39
  value: string;
36
40
  style?: 'primary' | 'secondary' | 'danger';
41
+ } | {
42
+ type: 'select';
43
+ id: string;
44
+ label?: string;
45
+ placeholder?: string;
46
+ options: StoredSelectOption[];
47
+ } | {
48
+ type: 'multi_select';
49
+ id: string;
50
+ label?: string;
51
+ options: StoredSelectOption[];
52
+ submitLabel?: string;
53
+ } | {
54
+ type: 'confirm';
55
+ id: string;
56
+ label?: string;
57
+ confirmLabel: string;
58
+ confirmValue: string;
59
+ cancelLabel?: string;
60
+ cancelValue?: string;
61
+ } | {
62
+ type: 'copy_button';
63
+ id: string;
64
+ label: string;
65
+ value: string;
37
66
  };
38
67
  interface ChatMessage {
39
68
  role: 'user' | 'assistant';
package/dist/plugin.d.ts CHANGED
@@ -21,6 +21,10 @@
21
21
  * DEVELOPING.md → "Publishing public @alfe.ai/* packages"). Keep in sync with
22
22
  * the transport union.
23
23
  */
24
+ interface StoredSelectOption {
25
+ label: string;
26
+ value: string;
27
+ }
24
28
  type StoredMessageComponent = {
25
29
  type: 'link_button';
26
30
  id: string;
@@ -34,6 +38,31 @@ type StoredMessageComponent = {
34
38
  label: string;
35
39
  value: string;
36
40
  style?: 'primary' | 'secondary' | 'danger';
41
+ } | {
42
+ type: 'select';
43
+ id: string;
44
+ label?: string;
45
+ placeholder?: string;
46
+ options: StoredSelectOption[];
47
+ } | {
48
+ type: 'multi_select';
49
+ id: string;
50
+ label?: string;
51
+ options: StoredSelectOption[];
52
+ submitLabel?: string;
53
+ } | {
54
+ type: 'confirm';
55
+ id: string;
56
+ label?: string;
57
+ confirmLabel: string;
58
+ confirmValue: string;
59
+ cancelLabel?: string;
60
+ cancelValue?: string;
61
+ } | {
62
+ type: 'copy_button';
63
+ id: string;
64
+ label: string;
65
+ value: string;
37
66
  };
38
67
  interface ChatMessage {
39
68
  role: 'user' | 'assistant';
package/dist/plugin2.cjs CHANGED
@@ -3,8 +3,8 @@ let node_fs_promises = require("node:fs/promises");
3
3
  let node_path = require("node:path");
4
4
  let node_os = require("node:os");
5
5
  let _alfe_ai_chat = require("@alfe.ai/chat");
6
- let node_crypto = require("node:crypto");
7
6
  let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
7
+ let node_crypto = require("node:crypto");
8
8
  let _alfe_ai_config = require("@alfe.ai/config");
9
9
  let node_fs = require("node:fs");
10
10
  //#region src/outbound-media.ts
@@ -376,24 +376,58 @@ function isSafeComponentUrl(raw) {
376
376
  }
377
377
  const MAX_COMPONENTS_PER_MESSAGE = 10;
378
378
  const MAX_COMPONENT_LABEL_CHARS = 120;
379
+ const MAX_COMPONENT_VALUE_CHARS = 400;
380
+ const MAX_OPTIONS_PER_COMPONENT = 25;
381
+ /** Cap a maybe-string; returns '' for non-strings so callers can `if (!x)`. */
382
+ function capStr(v, max) {
383
+ return typeof v === "string" ? v.slice(0, max) : "";
384
+ }
385
+ /**
386
+ * Validate + cap the {label,value} option list of a select / multi_select.
387
+ * Drops malformed / empty entries; caps the count. Empty result → the caller
388
+ * drops the whole component (a chooser with no choices is useless).
389
+ */
390
+ function sanitizeOptions(raw) {
391
+ if (!Array.isArray(raw)) return [];
392
+ const out = [];
393
+ for (const item of raw) {
394
+ if (out.length >= MAX_OPTIONS_PER_COMPONENT) break;
395
+ if (!item || typeof item !== "object") continue;
396
+ const o = item;
397
+ const label = capStr(o.label, MAX_COMPONENT_LABEL_CHARS);
398
+ const value = capStr(o.value, MAX_COMPONENT_VALUE_CHARS);
399
+ if (!label || !value) continue;
400
+ out.push({
401
+ label,
402
+ value
403
+ });
404
+ }
405
+ return out;
406
+ }
379
407
  /**
380
408
  * Validate + normalize agent-supplied components down to the persisted wire
381
- * shape. Drops anything malformed or unsafe (unsafe URL, missing fields,
382
- * over-cap). Returns at most MAX_COMPONENTS_PER_MESSAGE. Phase 1 renders
383
- * `link_button`; `quick_reply` is accepted for forward-compat persistence.
409
+ * shape. Drops anything malformed or unsafe (unsafe URL, missing required
410
+ * fields, no valid options, over-cap). Returns at most
411
+ * MAX_COMPONENTS_PER_MESSAGE. Handles the full core set: `link_button`,
412
+ * `quick_reply`, `select`, `multi_select`, `confirm`, `copy_button`. Only
413
+ * `link_button` is URL-safety-gated — the rest carry no URL, so their values
414
+ * are plain text (capped, never navigated).
384
415
  */
385
416
  function sanitizeComponents(raw) {
386
417
  if (!Array.isArray(raw)) return [];
387
418
  const out = [];
419
+ const usedIds = /* @__PURE__ */ new Set();
388
420
  for (const item of raw) {
389
421
  if (out.length >= MAX_COMPONENTS_PER_MESSAGE) break;
390
422
  if (!item || typeof item !== "object") continue;
391
423
  const c = item;
392
- const label = typeof c.label === "string" ? c.label.slice(0, MAX_COMPONENT_LABEL_CHARS) : "";
393
- if (!label) continue;
394
- const id = typeof c.id === "string" && c.id ? c.id : (0, node_crypto.randomUUID)();
424
+ const label = capStr(c.label, MAX_COMPONENT_LABEL_CHARS);
425
+ let id = typeof c.id === "string" && c.id ? c.id : (0, node_crypto.randomUUID)();
426
+ if (usedIds.has(id)) id = (0, node_crypto.randomUUID)();
427
+ usedIds.add(id);
395
428
  const style = c.style === "primary" || c.style === "secondary" || c.style === "danger" ? c.style : void 0;
396
429
  if (c.type === "link_button") {
430
+ if (!label) continue;
397
431
  if (typeof c.url !== "string" || !isSafeComponentUrl(c.url)) continue;
398
432
  const target = c.target === "same-tab" || c.target === "new-tab" || c.target === "popup" ? c.target : void 0;
399
433
  out.push({
@@ -405,14 +439,63 @@ function sanitizeComponents(raw) {
405
439
  ...style ? { style } : {}
406
440
  });
407
441
  } else if (c.type === "quick_reply") {
408
- if (typeof c.value !== "string" || !c.value) continue;
442
+ if (!label) continue;
443
+ const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
444
+ if (!value) continue;
409
445
  out.push({
410
446
  type: "quick_reply",
411
447
  id,
412
448
  label,
413
- value: c.value,
449
+ value,
414
450
  ...style ? { style } : {}
415
451
  });
452
+ } else if (c.type === "select") {
453
+ const options = sanitizeOptions(c.options);
454
+ if (options.length === 0) continue;
455
+ const placeholder = capStr(c.placeholder, MAX_COMPONENT_LABEL_CHARS);
456
+ out.push({
457
+ type: "select",
458
+ id,
459
+ ...label ? { label } : {},
460
+ ...placeholder ? { placeholder } : {},
461
+ options
462
+ });
463
+ } else if (c.type === "multi_select") {
464
+ const options = sanitizeOptions(c.options);
465
+ if (options.length === 0) continue;
466
+ const submitLabel = capStr(c.submitLabel, MAX_COMPONENT_LABEL_CHARS);
467
+ out.push({
468
+ type: "multi_select",
469
+ id,
470
+ ...label ? { label } : {},
471
+ options,
472
+ ...submitLabel ? { submitLabel } : {}
473
+ });
474
+ } else if (c.type === "confirm") {
475
+ const confirmLabel = capStr(c.confirmLabel, MAX_COMPONENT_LABEL_CHARS);
476
+ const confirmValue = capStr(c.confirmValue, MAX_COMPONENT_VALUE_CHARS);
477
+ if (!confirmLabel || !confirmValue) continue;
478
+ const cancelLabel = capStr(c.cancelLabel, MAX_COMPONENT_LABEL_CHARS);
479
+ const cancelValue = capStr(c.cancelValue, MAX_COMPONENT_VALUE_CHARS);
480
+ out.push({
481
+ type: "confirm",
482
+ id,
483
+ ...label ? { label } : {},
484
+ confirmLabel,
485
+ confirmValue,
486
+ ...cancelLabel ? { cancelLabel } : {},
487
+ ...cancelValue ? { cancelValue } : {}
488
+ });
489
+ } else if (c.type === "copy_button") {
490
+ if (!label) continue;
491
+ const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
492
+ if (!value) continue;
493
+ out.push({
494
+ type: "copy_button",
495
+ id,
496
+ label,
497
+ value
498
+ });
416
499
  }
417
500
  }
418
501
  return out;
@@ -1496,7 +1579,7 @@ function buildA2ATools(getChatClient, log, present) {
1496
1579
  ];
1497
1580
  if (present) tools.push(defineTool({
1498
1581
  name: "chat_present_components",
1499
- description: "Attach interactive UI components (e.g. a link button) to a chat message shown to the user. Provide the target conversation and, optionally, accompanying text shown above the components. Link URLs must be https on an Alfe-owned host. Components always degrade to a plain text link on clients that do not support them, so include the key info in `text` too.",
1582
+ description: "Attach interactive UI components to a chat message so the user can act with a tap instead of typing. PREFER components whenever you offer the user a discrete set of choices — a quick_reply or select reads far better than \"reply 1, 2, or 3\". Use:\n • quick_reply — one-tap canned replies for a short set of options.\n • select — a single choice from a longer list (dropdown).\n • multi_select — let the user pick MANY options, then submit.\n • confirm — an approval / yes-no decision (primary + secondary button).\n • link_button — an actionable LINK (open a dashboard, a browser-takeover deep link). URL must be https on an Alfe-owned host.\n • copy_button — let the user copy a value (token, id, snippet) to their clipboard.\nChoosing/submitting an interactive component sends the chosen value back AS THE USER'S NEXT MESSAGE, so the conversation continues normally — you will see it as ordinary user input. Don't button-spam: components are for real interactions, not decoration, and each message allows at most 10. Components degrade to plain text on clients that don't render them, so keep the essentials in `text` too.\nExamples:\n Approval: { to:\"conv:abc\", text:\"Deploy to production?\", components:[ { type:\"confirm\", id:\"c1\", confirmLabel:\"Deploy\", confirmValue:\"yes, deploy\", cancelLabel:\"Cancel\", cancelValue:\"no, hold off\" } ] }\n Pick one: { to:\"user:u_123\", text:\"Which environment?\", components:[ { type:\"select\", id:\"s1\", placeholder:\"Choose an environment\", options:[ { label:\"Dev\", value:\"dev\" }, { label:\"Staging\", value:\"staging\" }, { label:\"Prod\", value:\"prod\" } ] } ] }\n Link: { to:\"conv:abc\", text:\"I need you to finish the login step.\", components:[ { type:\"link_button\", id:\"b1\", label:\"Open browser session\", url:\"https://app.alfe.ai/agents/x?tab=browser\", target:\"new-tab\", style:\"primary\" } ] }",
1500
1583
  parameters: {
1501
1584
  type: "object",
1502
1585
  properties: {
@@ -1506,26 +1589,37 @@ function buildA2ATools(getChatClient, log, present) {
1506
1589
  },
1507
1590
  text: {
1508
1591
  type: "string",
1509
- description: "Optional message text rendered above the components."
1592
+ description: "Optional message text rendered above the components. Keep the key info here too — it is the fallback on clients that cannot render components."
1510
1593
  },
1511
1594
  components: {
1512
1595
  type: "array",
1513
- description: "Interactive components to attach (at least one).",
1596
+ description: "Interactive components to attach (at least one, at most 10).",
1514
1597
  items: {
1515
1598
  type: "object",
1516
1599
  properties: {
1517
1600
  type: {
1518
1601
  type: "string",
1519
- enum: ["link_button"],
1520
- description: "Component kind. Phase 1 supports link_button."
1602
+ enum: [
1603
+ "link_button",
1604
+ "quick_reply",
1605
+ "select",
1606
+ "multi_select",
1607
+ "confirm",
1608
+ "copy_button"
1609
+ ],
1610
+ description: "Component kind — see the tool description for when to use each."
1611
+ },
1612
+ id: {
1613
+ type: "string",
1614
+ description: "Stable id (optional — auto-generated when omitted)."
1521
1615
  },
1522
1616
  label: {
1523
1617
  type: "string",
1524
- description: "Button text."
1618
+ description: "Button/control text. Required for link_button, quick_reply, copy_button; optional heading for select / multi_select."
1525
1619
  },
1526
1620
  url: {
1527
1621
  type: "string",
1528
- description: "https URL on an Alfe-owned host to open when clicked."
1622
+ description: "link_button only: https URL on an Alfe-owned host to open when clicked."
1529
1623
  },
1530
1624
  target: {
1531
1625
  type: "string",
@@ -1534,7 +1628,53 @@ function buildA2ATools(getChatClient, log, present) {
1534
1628
  "new-tab",
1535
1629
  "popup"
1536
1630
  ],
1537
- description: "How the URL opens (default same-tab)."
1631
+ description: "link_button only: how the URL opens (default same-tab)."
1632
+ },
1633
+ value: {
1634
+ type: "string",
1635
+ description: "quick_reply: the text submitted as the user's next message when tapped. copy_button: the value copied to the clipboard."
1636
+ },
1637
+ placeholder: {
1638
+ type: "string",
1639
+ description: "select only: the empty-state prompt shown before a choice is made."
1640
+ },
1641
+ options: {
1642
+ type: "array",
1643
+ description: "select / multi_select only: the choices (max 25). Each is { label, value }; `value` is what gets submitted (multi_select joins checked values with \", \").",
1644
+ items: {
1645
+ type: "object",
1646
+ properties: {
1647
+ label: {
1648
+ type: "string",
1649
+ description: "Option text shown to the user."
1650
+ },
1651
+ value: {
1652
+ type: "string",
1653
+ description: "Value submitted when chosen."
1654
+ }
1655
+ },
1656
+ required: ["label", "value"]
1657
+ }
1658
+ },
1659
+ submitLabel: {
1660
+ type: "string",
1661
+ description: "multi_select only: submit button caption (default \"Submit\")."
1662
+ },
1663
+ confirmLabel: {
1664
+ type: "string",
1665
+ description: "confirm only: primary button text (e.g. \"Approve\")."
1666
+ },
1667
+ confirmValue: {
1668
+ type: "string",
1669
+ description: "confirm only: value submitted when the primary button is tapped."
1670
+ },
1671
+ cancelLabel: {
1672
+ type: "string",
1673
+ description: "confirm only: secondary button text (omit to show only the primary)."
1674
+ },
1675
+ cancelValue: {
1676
+ type: "string",
1677
+ description: "confirm only: value submitted when the secondary button is tapped."
1538
1678
  },
1539
1679
  style: {
1540
1680
  type: "string",
@@ -1543,14 +1683,10 @@ function buildA2ATools(getChatClient, log, present) {
1543
1683
  "secondary",
1544
1684
  "danger"
1545
1685
  ],
1546
- description: "Visual style hint."
1686
+ description: "link_button / quick_reply only: visual style hint."
1547
1687
  }
1548
1688
  },
1549
- required: [
1550
- "type",
1551
- "label",
1552
- "url"
1553
- ]
1689
+ required: ["type"]
1554
1690
  }
1555
1691
  }
1556
1692
  },
@@ -2516,6 +2652,7 @@ const plugin = {
2516
2652
  api.registerChannel(alfeChannel);
2517
2653
  log.info(`Registered channel: ${alfeChannel.id}`);
2518
2654
  if (typeof api.registerTool === "function") {
2655
+ (0, _alfe_ai_agent_api_client.installToolErrorCapture)(api, { plugin: "openclaw-chat" });
2519
2656
  const present = async (args) => {
2520
2657
  const clean = sanitizeComponents(args.components);
2521
2658
  if (clean.length === 0) throw new Error("No valid components: link_button requires a `label` and an https `url` on an Alfe-owned host");
package/dist/plugin2.js CHANGED
@@ -3,8 +3,8 @@ import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "node:fs
3
3
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
6
+ import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
6
7
  import { randomUUID } from "node:crypto";
7
- import { AgentApiClient } from "@alfe.ai/agent-api-client";
8
8
  import { resolveConfig } from "@alfe.ai/config";
9
9
  import { existsSync } from "node:fs";
10
10
  //#region src/outbound-media.ts
@@ -376,24 +376,58 @@ function isSafeComponentUrl(raw) {
376
376
  }
377
377
  const MAX_COMPONENTS_PER_MESSAGE = 10;
378
378
  const MAX_COMPONENT_LABEL_CHARS = 120;
379
+ const MAX_COMPONENT_VALUE_CHARS = 400;
380
+ const MAX_OPTIONS_PER_COMPONENT = 25;
381
+ /** Cap a maybe-string; returns '' for non-strings so callers can `if (!x)`. */
382
+ function capStr(v, max) {
383
+ return typeof v === "string" ? v.slice(0, max) : "";
384
+ }
385
+ /**
386
+ * Validate + cap the {label,value} option list of a select / multi_select.
387
+ * Drops malformed / empty entries; caps the count. Empty result → the caller
388
+ * drops the whole component (a chooser with no choices is useless).
389
+ */
390
+ function sanitizeOptions(raw) {
391
+ if (!Array.isArray(raw)) return [];
392
+ const out = [];
393
+ for (const item of raw) {
394
+ if (out.length >= MAX_OPTIONS_PER_COMPONENT) break;
395
+ if (!item || typeof item !== "object") continue;
396
+ const o = item;
397
+ const label = capStr(o.label, MAX_COMPONENT_LABEL_CHARS);
398
+ const value = capStr(o.value, MAX_COMPONENT_VALUE_CHARS);
399
+ if (!label || !value) continue;
400
+ out.push({
401
+ label,
402
+ value
403
+ });
404
+ }
405
+ return out;
406
+ }
379
407
  /**
380
408
  * Validate + normalize agent-supplied components down to the persisted wire
381
- * shape. Drops anything malformed or unsafe (unsafe URL, missing fields,
382
- * over-cap). Returns at most MAX_COMPONENTS_PER_MESSAGE. Phase 1 renders
383
- * `link_button`; `quick_reply` is accepted for forward-compat persistence.
409
+ * shape. Drops anything malformed or unsafe (unsafe URL, missing required
410
+ * fields, no valid options, over-cap). Returns at most
411
+ * MAX_COMPONENTS_PER_MESSAGE. Handles the full core set: `link_button`,
412
+ * `quick_reply`, `select`, `multi_select`, `confirm`, `copy_button`. Only
413
+ * `link_button` is URL-safety-gated — the rest carry no URL, so their values
414
+ * are plain text (capped, never navigated).
384
415
  */
385
416
  function sanitizeComponents(raw) {
386
417
  if (!Array.isArray(raw)) return [];
387
418
  const out = [];
419
+ const usedIds = /* @__PURE__ */ new Set();
388
420
  for (const item of raw) {
389
421
  if (out.length >= MAX_COMPONENTS_PER_MESSAGE) break;
390
422
  if (!item || typeof item !== "object") continue;
391
423
  const c = item;
392
- const label = typeof c.label === "string" ? c.label.slice(0, MAX_COMPONENT_LABEL_CHARS) : "";
393
- if (!label) continue;
394
- const id = typeof c.id === "string" && c.id ? c.id : randomUUID();
424
+ const label = capStr(c.label, MAX_COMPONENT_LABEL_CHARS);
425
+ let id = typeof c.id === "string" && c.id ? c.id : randomUUID();
426
+ if (usedIds.has(id)) id = randomUUID();
427
+ usedIds.add(id);
395
428
  const style = c.style === "primary" || c.style === "secondary" || c.style === "danger" ? c.style : void 0;
396
429
  if (c.type === "link_button") {
430
+ if (!label) continue;
397
431
  if (typeof c.url !== "string" || !isSafeComponentUrl(c.url)) continue;
398
432
  const target = c.target === "same-tab" || c.target === "new-tab" || c.target === "popup" ? c.target : void 0;
399
433
  out.push({
@@ -405,14 +439,63 @@ function sanitizeComponents(raw) {
405
439
  ...style ? { style } : {}
406
440
  });
407
441
  } else if (c.type === "quick_reply") {
408
- if (typeof c.value !== "string" || !c.value) continue;
442
+ if (!label) continue;
443
+ const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
444
+ if (!value) continue;
409
445
  out.push({
410
446
  type: "quick_reply",
411
447
  id,
412
448
  label,
413
- value: c.value,
449
+ value,
414
450
  ...style ? { style } : {}
415
451
  });
452
+ } else if (c.type === "select") {
453
+ const options = sanitizeOptions(c.options);
454
+ if (options.length === 0) continue;
455
+ const placeholder = capStr(c.placeholder, MAX_COMPONENT_LABEL_CHARS);
456
+ out.push({
457
+ type: "select",
458
+ id,
459
+ ...label ? { label } : {},
460
+ ...placeholder ? { placeholder } : {},
461
+ options
462
+ });
463
+ } else if (c.type === "multi_select") {
464
+ const options = sanitizeOptions(c.options);
465
+ if (options.length === 0) continue;
466
+ const submitLabel = capStr(c.submitLabel, MAX_COMPONENT_LABEL_CHARS);
467
+ out.push({
468
+ type: "multi_select",
469
+ id,
470
+ ...label ? { label } : {},
471
+ options,
472
+ ...submitLabel ? { submitLabel } : {}
473
+ });
474
+ } else if (c.type === "confirm") {
475
+ const confirmLabel = capStr(c.confirmLabel, MAX_COMPONENT_LABEL_CHARS);
476
+ const confirmValue = capStr(c.confirmValue, MAX_COMPONENT_VALUE_CHARS);
477
+ if (!confirmLabel || !confirmValue) continue;
478
+ const cancelLabel = capStr(c.cancelLabel, MAX_COMPONENT_LABEL_CHARS);
479
+ const cancelValue = capStr(c.cancelValue, MAX_COMPONENT_VALUE_CHARS);
480
+ out.push({
481
+ type: "confirm",
482
+ id,
483
+ ...label ? { label } : {},
484
+ confirmLabel,
485
+ confirmValue,
486
+ ...cancelLabel ? { cancelLabel } : {},
487
+ ...cancelValue ? { cancelValue } : {}
488
+ });
489
+ } else if (c.type === "copy_button") {
490
+ if (!label) continue;
491
+ const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
492
+ if (!value) continue;
493
+ out.push({
494
+ type: "copy_button",
495
+ id,
496
+ label,
497
+ value
498
+ });
416
499
  }
417
500
  }
418
501
  return out;
@@ -1496,7 +1579,7 @@ function buildA2ATools(getChatClient, log, present) {
1496
1579
  ];
1497
1580
  if (present) tools.push(defineTool({
1498
1581
  name: "chat_present_components",
1499
- description: "Attach interactive UI components (e.g. a link button) to a chat message shown to the user. Provide the target conversation and, optionally, accompanying text shown above the components. Link URLs must be https on an Alfe-owned host. Components always degrade to a plain text link on clients that do not support them, so include the key info in `text` too.",
1582
+ description: "Attach interactive UI components to a chat message so the user can act with a tap instead of typing. PREFER components whenever you offer the user a discrete set of choices — a quick_reply or select reads far better than \"reply 1, 2, or 3\". Use:\n • quick_reply — one-tap canned replies for a short set of options.\n • select — a single choice from a longer list (dropdown).\n • multi_select — let the user pick MANY options, then submit.\n • confirm — an approval / yes-no decision (primary + secondary button).\n • link_button — an actionable LINK (open a dashboard, a browser-takeover deep link). URL must be https on an Alfe-owned host.\n • copy_button — let the user copy a value (token, id, snippet) to their clipboard.\nChoosing/submitting an interactive component sends the chosen value back AS THE USER'S NEXT MESSAGE, so the conversation continues normally — you will see it as ordinary user input. Don't button-spam: components are for real interactions, not decoration, and each message allows at most 10. Components degrade to plain text on clients that don't render them, so keep the essentials in `text` too.\nExamples:\n Approval: { to:\"conv:abc\", text:\"Deploy to production?\", components:[ { type:\"confirm\", id:\"c1\", confirmLabel:\"Deploy\", confirmValue:\"yes, deploy\", cancelLabel:\"Cancel\", cancelValue:\"no, hold off\" } ] }\n Pick one: { to:\"user:u_123\", text:\"Which environment?\", components:[ { type:\"select\", id:\"s1\", placeholder:\"Choose an environment\", options:[ { label:\"Dev\", value:\"dev\" }, { label:\"Staging\", value:\"staging\" }, { label:\"Prod\", value:\"prod\" } ] } ] }\n Link: { to:\"conv:abc\", text:\"I need you to finish the login step.\", components:[ { type:\"link_button\", id:\"b1\", label:\"Open browser session\", url:\"https://app.alfe.ai/agents/x?tab=browser\", target:\"new-tab\", style:\"primary\" } ] }",
1500
1583
  parameters: {
1501
1584
  type: "object",
1502
1585
  properties: {
@@ -1506,26 +1589,37 @@ function buildA2ATools(getChatClient, log, present) {
1506
1589
  },
1507
1590
  text: {
1508
1591
  type: "string",
1509
- description: "Optional message text rendered above the components."
1592
+ description: "Optional message text rendered above the components. Keep the key info here too — it is the fallback on clients that cannot render components."
1510
1593
  },
1511
1594
  components: {
1512
1595
  type: "array",
1513
- description: "Interactive components to attach (at least one).",
1596
+ description: "Interactive components to attach (at least one, at most 10).",
1514
1597
  items: {
1515
1598
  type: "object",
1516
1599
  properties: {
1517
1600
  type: {
1518
1601
  type: "string",
1519
- enum: ["link_button"],
1520
- description: "Component kind. Phase 1 supports link_button."
1602
+ enum: [
1603
+ "link_button",
1604
+ "quick_reply",
1605
+ "select",
1606
+ "multi_select",
1607
+ "confirm",
1608
+ "copy_button"
1609
+ ],
1610
+ description: "Component kind — see the tool description for when to use each."
1611
+ },
1612
+ id: {
1613
+ type: "string",
1614
+ description: "Stable id (optional — auto-generated when omitted)."
1521
1615
  },
1522
1616
  label: {
1523
1617
  type: "string",
1524
- description: "Button text."
1618
+ description: "Button/control text. Required for link_button, quick_reply, copy_button; optional heading for select / multi_select."
1525
1619
  },
1526
1620
  url: {
1527
1621
  type: "string",
1528
- description: "https URL on an Alfe-owned host to open when clicked."
1622
+ description: "link_button only: https URL on an Alfe-owned host to open when clicked."
1529
1623
  },
1530
1624
  target: {
1531
1625
  type: "string",
@@ -1534,7 +1628,53 @@ function buildA2ATools(getChatClient, log, present) {
1534
1628
  "new-tab",
1535
1629
  "popup"
1536
1630
  ],
1537
- description: "How the URL opens (default same-tab)."
1631
+ description: "link_button only: how the URL opens (default same-tab)."
1632
+ },
1633
+ value: {
1634
+ type: "string",
1635
+ description: "quick_reply: the text submitted as the user's next message when tapped. copy_button: the value copied to the clipboard."
1636
+ },
1637
+ placeholder: {
1638
+ type: "string",
1639
+ description: "select only: the empty-state prompt shown before a choice is made."
1640
+ },
1641
+ options: {
1642
+ type: "array",
1643
+ description: "select / multi_select only: the choices (max 25). Each is { label, value }; `value` is what gets submitted (multi_select joins checked values with \", \").",
1644
+ items: {
1645
+ type: "object",
1646
+ properties: {
1647
+ label: {
1648
+ type: "string",
1649
+ description: "Option text shown to the user."
1650
+ },
1651
+ value: {
1652
+ type: "string",
1653
+ description: "Value submitted when chosen."
1654
+ }
1655
+ },
1656
+ required: ["label", "value"]
1657
+ }
1658
+ },
1659
+ submitLabel: {
1660
+ type: "string",
1661
+ description: "multi_select only: submit button caption (default \"Submit\")."
1662
+ },
1663
+ confirmLabel: {
1664
+ type: "string",
1665
+ description: "confirm only: primary button text (e.g. \"Approve\")."
1666
+ },
1667
+ confirmValue: {
1668
+ type: "string",
1669
+ description: "confirm only: value submitted when the primary button is tapped."
1670
+ },
1671
+ cancelLabel: {
1672
+ type: "string",
1673
+ description: "confirm only: secondary button text (omit to show only the primary)."
1674
+ },
1675
+ cancelValue: {
1676
+ type: "string",
1677
+ description: "confirm only: value submitted when the secondary button is tapped."
1538
1678
  },
1539
1679
  style: {
1540
1680
  type: "string",
@@ -1543,14 +1683,10 @@ function buildA2ATools(getChatClient, log, present) {
1543
1683
  "secondary",
1544
1684
  "danger"
1545
1685
  ],
1546
- description: "Visual style hint."
1686
+ description: "link_button / quick_reply only: visual style hint."
1547
1687
  }
1548
1688
  },
1549
- required: [
1550
- "type",
1551
- "label",
1552
- "url"
1553
- ]
1689
+ required: ["type"]
1554
1690
  }
1555
1691
  }
1556
1692
  },
@@ -2516,6 +2652,7 @@ const plugin = {
2516
2652
  api.registerChannel(alfeChannel);
2517
2653
  log.info(`Registered channel: ${alfeChannel.id}`);
2518
2654
  if (typeof api.registerTool === "function") {
2655
+ installToolErrorCapture(api, { plugin: "openclaw-chat" });
2519
2656
  const present = async (args) => {
2520
2657
  const clean = sanitizeComponents(args.components);
2521
2658
  if (clean.length === 0) throw new Error("No valid components: link_button requires a `label` and an https `url` on an Alfe-owned host");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -27,7 +27,7 @@
27
27
  "openclaw.plugin.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@alfe.ai/agent-api-client": "^0.7.0",
30
+ "@alfe.ai/agent-api-client": "^0.8.0",
31
31
  "@alfe.ai/chat": "^0.0.14",
32
32
  "@alfe.ai/config": "^0.3.0"
33
33
  },