@peers-app/peers-ui 0.25.20 → 0.26.0

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 (41) hide show
  1. package/dist/mention-configs.d.ts +7 -0
  2. package/dist/mention-configs.js +8 -2
  3. package/dist/screens/assistants/assistant-config.d.ts +1 -0
  4. package/dist/screens/assistants/assistant-config.js +20 -3
  5. package/dist/screens/assistants/assistant-config.test.d.ts +1 -0
  6. package/dist/screens/assistants/assistant-config.test.js +57 -0
  7. package/dist/screens/settings/settings-page.js +1 -59
  8. package/dist/screens/settings/settings-page.test.d.ts +1 -0
  9. package/dist/screens/settings/settings-page.test.js +19 -0
  10. package/dist/screens/tools/tool-catalog-ipc.d.ts +118 -0
  11. package/dist/screens/tools/tool-catalog-ipc.js +48 -0
  12. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +1 -0
  13. package/dist/screens/tools/tool-catalog-ipc.test.js +46 -0
  14. package/dist/screens/tools/tool-details.d.ts +3 -4
  15. package/dist/screens/tools/tool-details.js +14 -33
  16. package/dist/screens/tools/tool-info.d.ts +5 -4
  17. package/dist/screens/tools/tool-info.js +10 -30
  18. package/dist/screens/tools/tool-list.d.ts +1 -0
  19. package/dist/screens/tools/tool-list.js +2 -26
  20. package/dist/screens/tools/tool-schema.d.ts +5 -3
  21. package/dist/screens/tools/tool-schema.js +5 -14
  22. package/dist/ui-router/ui-loader.js +14 -27
  23. package/dist/ui-router/ui-loader.test.d.ts +1 -0
  24. package/dist/ui-router/ui-loader.test.js +91 -0
  25. package/package.json +3 -3
  26. package/src/mention-configs.ts +9 -5
  27. package/src/screens/assistants/assistant-config.test.tsx +54 -0
  28. package/src/screens/assistants/assistant-config.tsx +42 -11
  29. package/src/screens/settings/settings-page.test.tsx +24 -0
  30. package/src/screens/settings/settings-page.tsx +1 -255
  31. package/src/screens/tools/tool-catalog-ipc.test.ts +46 -0
  32. package/src/screens/tools/tool-catalog-ipc.ts +51 -0
  33. package/src/screens/tools/tool-details.tsx +18 -67
  34. package/src/screens/tools/tool-info.tsx +39 -53
  35. package/src/screens/tools/tool-list.tsx +4 -37
  36. package/src/screens/tools/tool-schema.tsx +40 -45
  37. package/src/ui-router/ui-loader.test.tsx +108 -0
  38. package/src/ui-router/ui-loader.tsx +15 -46
  39. package/dist/screens/tools/tool-code.d.ts +0 -4
  40. package/dist/screens/tools/tool-code.js +0 -28
  41. package/src/screens/tools/tool-code.tsx +0 -30
@@ -8,7 +8,7 @@ import {
8
8
  Users,
9
9
  } from "@peers-app/peers-sdk";
10
10
  import type React from "react";
11
- import { useEffect, useRef, useState } from "react";
11
+ import { useEffect, useState } from "react";
12
12
  import { Input } from "../../components/input";
13
13
  import { Tabs } from "../../components/tabs";
14
14
  import { Tooltip } from "../../components/tooltip";
@@ -20,9 +20,6 @@ import { IDENTITY_ACCOUNT_PATH, IDENTITY_PAIR_PATH } from "../identity/identity-
20
20
  import { getNetworkControlConsumer } from "../network-viewer/network-viewer-ipc";
21
21
  import { ColorModeDropdown } from "./color-mode-dropdown";
22
22
 
23
- /** Electron file picker attaches a filesystem path on `File`. */
24
- type FileWithElectronPath = File & { path?: string };
25
-
26
23
  type PeersHostWindow = Window & {
27
24
  electronAPI?: unknown;
28
25
  ReactNativeWebView?: unknown;
@@ -106,7 +103,6 @@ const AdvancedSettingsTab: React.FC = () => {
106
103
  <ResetDeviceSyncInfos />
107
104
  <DeleteLocalDatabase />
108
105
  <CleanupOldDataDir />
109
- <ImportOldPeersData />
110
106
  <ResetChangeTracking />
111
107
  </>
112
108
  );
@@ -403,256 +399,6 @@ const ResetChangeTracking: React.FC = () => {
403
399
  );
404
400
  };
405
401
 
406
- const IMPORT_TOOL_ID = "00mh0wlipkdbeaw8imptsk001";
407
-
408
- interface IDryRunResult {
409
- totalRecords: number;
410
- totalTasks: number;
411
- totalLogEntries: number;
412
- groups: {
413
- groupId: string;
414
- groupName: string;
415
- context: "personal" | "home";
416
- taskCount: number;
417
- activeCount: number;
418
- doneCount: number;
419
- rootTaskCount: number;
420
- subtaskCount: number;
421
- logEntryCount: number;
422
- }[];
423
- statusMapping: {
424
- inProgressToDone: number;
425
- inProgressKeep: number;
426
- queuedToDone: number;
427
- queuedKeep: number;
428
- backlogToDone: number;
429
- backlogKeep: number;
430
- noStatusToDone: number;
431
- noStatusToBacklog: number;
432
- };
433
- userMapping: {
434
- mark: number;
435
- blair: number;
436
- other: number;
437
- };
438
- warnings: string[];
439
- }
440
-
441
- const ImportOldPeersData: React.FC = () => {
442
- const fileInputRef = useRef<HTMLInputElement>(null);
443
- const [filePath, setFilePath] = useState<string>("");
444
- const [_fileName, setFileName] = useState<string>("");
445
- const [loading, setLoading] = useState(false);
446
- const [result, setResult] = useState<IDryRunResult | null>(null);
447
- const [error, setError] = useState<string>("");
448
-
449
- const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
450
- const file = e.target.files?.[0];
451
- if (!file) return;
452
- // Electron exposes .path on File objects
453
- const path = (file as FileWithElectronPath).path;
454
- if (path) {
455
- setFilePath(path);
456
- setFileName(file.name);
457
- } else {
458
- setFileName(file.name);
459
- setFilePath("");
460
- setError("File path not available. This feature requires the Electron desktop app.");
461
- }
462
- setResult(null);
463
- setError("");
464
- };
465
-
466
- const handleDryRun = async () => {
467
- if (!filePath) return;
468
- setLoading(true);
469
- setError("");
470
- setResult(null);
471
- try {
472
- const response = await rpcServerCalls.runTool(IMPORT_TOOL_ID, {
473
- filePath,
474
- dryRun: true,
475
- });
476
- if (response?.result) {
477
- setResult(response.result);
478
- } else {
479
- setError("Unexpected response from import tool");
480
- }
481
- } catch (err) {
482
- setError((err as Error).message || "Failed to run dry run");
483
- } finally {
484
- setLoading(false);
485
- }
486
- };
487
-
488
- return (
489
- <div className="mt-4 pt-3 border-top">
490
- <h6 className="mb-2">Import Old Peers Data</h6>
491
- <small className="text-muted d-block mb-2">
492
- Import tasks and log entries from an old peers JSON export file.
493
- </small>
494
-
495
- <div className="d-flex align-items-center gap-2 mb-2">
496
- <input
497
- ref={fileInputRef}
498
- type="file"
499
- accept=".json"
500
- className="form-control form-control-sm"
501
- style={{ maxWidth: 350 }}
502
- onChange={handleFileSelect}
503
- />
504
- <button
505
- className="btn btn-outline-primary btn-sm"
506
- onClick={handleDryRun}
507
- disabled={!filePath || loading}
508
- >
509
- {loading ? "Analyzing..." : "Dry Run"}
510
- </button>
511
- </div>
512
-
513
- {error && <div className="alert alert-danger py-1 px-2 small mt-2">{error}</div>}
514
-
515
- {result && <DryRunResults result={result} />}
516
- </div>
517
- );
518
- };
519
-
520
- const DryRunResults: React.FC<{ result: IDryRunResult }> = ({ result }) => {
521
- const sm = result.statusMapping;
522
- const um = result.userMapping;
523
-
524
- return (
525
- <div className="mt-2">
526
- <div className="alert alert-info py-2 px-3 small">
527
- <strong>Summary:</strong> {result.totalTasks.toLocaleString()} tasks and{" "}
528
- {result.totalLogEntries.toLocaleString()} log entries across {result.groups.length} groups (
529
- {result.totalRecords.toLocaleString()} total records in file)
530
- </div>
531
-
532
- {result.warnings.length > 0 && (
533
- <div className="alert alert-warning py-2 px-3 small">
534
- <strong>Warnings:</strong>
535
- <ul className="mb-0 ps-3">
536
- {result.warnings.map((w, i) => (
537
- <li key={i}>{w}</li>
538
- ))}
539
- </ul>
540
- </div>
541
- )}
542
-
543
- <details className="mb-2">
544
- <summary className="small fw-bold" style={{ cursor: "pointer" }}>
545
- Status Mapping
546
- </summary>
547
- <table className="table table-sm small mt-1">
548
- <thead>
549
- <tr>
550
- <th>Mapping</th>
551
- <th className="text-end">Count</th>
552
- </tr>
553
- </thead>
554
- <tbody>
555
- <tr>
556
- <td>In-Progress → Done</td>
557
- <td className="text-end">{sm.inProgressToDone.toLocaleString()}</td>
558
- </tr>
559
- <tr>
560
- <td>In-Progress → In-Progress</td>
561
- <td className="text-end">{sm.inProgressKeep.toLocaleString()}</td>
562
- </tr>
563
- <tr>
564
- <td>Queued → Done</td>
565
- <td className="text-end">{sm.queuedToDone.toLocaleString()}</td>
566
- </tr>
567
- <tr>
568
- <td>Queued → Queued</td>
569
- <td className="text-end">{sm.queuedKeep.toLocaleString()}</td>
570
- </tr>
571
- <tr>
572
- <td>Backlog → Done</td>
573
- <td className="text-end">{sm.backlogToDone.toLocaleString()}</td>
574
- </tr>
575
- <tr>
576
- <td>Backlog → Backlog</td>
577
- <td className="text-end">{sm.backlogKeep.toLocaleString()}</td>
578
- </tr>
579
- <tr>
580
- <td>No status → Done</td>
581
- <td className="text-end">{sm.noStatusToDone.toLocaleString()}</td>
582
- </tr>
583
- <tr>
584
- <td>No status → Backlog</td>
585
- <td className="text-end">{sm.noStatusToBacklog.toLocaleString()}</td>
586
- </tr>
587
- </tbody>
588
- </table>
589
- </details>
590
-
591
- <details className="mb-2">
592
- <summary className="small fw-bold" style={{ cursor: "pointer" }}>
593
- User Mapping
594
- </summary>
595
- <table className="table table-sm small mt-1">
596
- <thead>
597
- <tr>
598
- <th>User</th>
599
- <th className="text-end">Tasks</th>
600
- </tr>
601
- </thead>
602
- <tbody>
603
- <tr>
604
- <td>Mark</td>
605
- <td className="text-end">{um.mark.toLocaleString()}</td>
606
- </tr>
607
- <tr>
608
- <td>Blair</td>
609
- <td className="text-end">{um.blair.toLocaleString()}</td>
610
- </tr>
611
- <tr>
612
- <td>Other → Mark</td>
613
- <td className="text-end">{um.other.toLocaleString()}</td>
614
- </tr>
615
- </tbody>
616
- </table>
617
- </details>
618
-
619
- <details open className="mb-2">
620
- <summary className="small fw-bold" style={{ cursor: "pointer" }}>
621
- Groups ({result.groups.length})
622
- </summary>
623
- <table className="table table-sm small mt-1">
624
- <thead>
625
- <tr>
626
- <th>Group</th>
627
- <th>Route</th>
628
- <th className="text-end">Tasks</th>
629
- <th className="text-end">Active</th>
630
- <th className="text-end">Done</th>
631
- <th className="text-end">Logs</th>
632
- </tr>
633
- </thead>
634
- <tbody>
635
- {result.groups.map((g) => (
636
- <tr key={g.groupId}>
637
- <td>{g.groupName}</td>
638
- <td>
639
- <span className={`badge bg-${g.context === "home" ? "success" : "secondary"}`}>
640
- {g.context}
641
- </span>
642
- </td>
643
- <td className="text-end">{g.taskCount.toLocaleString()}</td>
644
- <td className="text-end">{g.activeCount.toLocaleString()}</td>
645
- <td className="text-end">{g.doneCount.toLocaleString()}</td>
646
- <td className="text-end">{g.logEntryCount.toLocaleString()}</td>
647
- </tr>
648
- ))}
649
- </tbody>
650
- </table>
651
- </details>
652
- </div>
653
- );
654
- };
655
-
656
402
  const LogoutSection: React.FC = () => {
657
403
  if (!isBrowserPwaHost) return null;
658
404
  return (
@@ -0,0 +1,46 @@
1
+ const list = jest.fn();
2
+ const get = jest.fn();
3
+ const rows = jest.fn();
4
+ let transport = true;
5
+ jest.mock("@peers-app/peers-sdk", () => ({
6
+ getDefaultDataContext: async () => ({ dataContextId: "group" }),
7
+ getHostContractTransport: () => (transport ? {} : undefined),
8
+ getToolCatalogContractDefinition: () => ({}),
9
+ hostContractConsumer: () => ({ get: async () => ({ tools: { listTools: list, getTool: get } }) }),
10
+ Tools: () => ({ list: rows, get: async (toolId: string) => ({ toolId, name: "historical" }) }),
11
+ }));
12
+
13
+ import { toolMentionConfig } from "../../mention-configs";
14
+ import { getToolDetails, listWorkflowMentionTools } from "./tool-catalog-ipc";
15
+
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ transport = true;
19
+ });
20
+ test("tool mentions include only workflow-invocable catalog tools", async () => {
21
+ list.mockResolvedValue({
22
+ tools: [
23
+ { toolId: "run", name: "Run", workflowInvocable: true },
24
+ { toolId: "approve", name: "Approve", workflowInvocable: false },
25
+ ],
26
+ });
27
+ expect(await toolMentionConfig.query("run")).toEqual([{ kind: "tool", id: "run", name: "Run" }]);
28
+ expect(rows).not.toHaveBeenCalled();
29
+ expect(await toolMentionConfig.getById("old")).toEqual({
30
+ kind: "tool",
31
+ id: "old",
32
+ name: "historical",
33
+ });
34
+ });
35
+ test("browser fallback omits quarantined rows and preserves historical detail", async () => {
36
+ transport = false;
37
+ rows.mockResolvedValue([
38
+ { toolId: "old", name: "Old" },
39
+ { toolId: "bad", name: "Bad", catalogState: "quarantined" },
40
+ ]);
41
+ expect(await listWorkflowMentionTools("", 10)).toEqual([{ toolId: "old", name: "Old" }]);
42
+ expect(await getToolDetails("old")).toEqual({
43
+ row: { toolId: "old", name: "historical" },
44
+ entry: undefined,
45
+ });
46
+ });
@@ -0,0 +1,51 @@
1
+ import {
2
+ getDefaultDataContext,
3
+ getHostContractTransport,
4
+ getToolCatalogContractDefinition,
5
+ hostContractConsumer,
6
+ type IHostContractConsumer,
7
+ type IToolCatalogConsumer,
8
+ type IToolCatalogEntry,
9
+ Tools,
10
+ } from "@peers-app/peers-sdk";
11
+
12
+ const consumers = new Map<string, IHostContractConsumer<IToolCatalogConsumer>>();
13
+ /** Obtain a catalog consumer for the selected host context; PWA callers can fall back to rows. */
14
+ export async function getToolCatalogConsumer(
15
+ dataContextId?: string,
16
+ ): Promise<IToolCatalogConsumer> {
17
+ if (!getHostContractTransport()) throw new Error("Tool catalog host transport unavailable");
18
+ dataContextId ??= (await getDefaultDataContext()).dataContextId;
19
+ let handle = consumers.get(dataContextId);
20
+ if (!handle) {
21
+ const id = dataContextId;
22
+ handle = hostContractConsumer<IToolCatalogConsumer>(getToolCatalogContractDefinition(), {
23
+ dataContextId: async () => id,
24
+ });
25
+ consumers.set(id, handle);
26
+ }
27
+ return handle.get();
28
+ }
29
+ /** Get live tool details, retaining historical row metadata when the host catalog is absent. */
30
+ export async function getToolDetails(toolId: string) {
31
+ const row = await Tools().get(toolId);
32
+ if (!row) return undefined;
33
+ let entry: IToolCatalogEntry | undefined;
34
+ try {
35
+ entry = (await (await getToolCatalogConsumer()).tools.getTool({ toolIdOrName: toolId })).tool;
36
+ } catch {
37
+ /* Browser-only hosts retain catalog rows. */
38
+ }
39
+ return { row, entry };
40
+ }
41
+ /** Query workflow mention candidates; unavailable browser hosts retain non-quarantined rows. */
42
+ export async function listWorkflowMentionTools(query: string, limit: number) {
43
+ try {
44
+ const { tools } = await (await getToolCatalogConsumer()).tools.listTools({ query, limit: 500 });
45
+ return tools.filter((entry) => entry.workflowInvocable).slice(0, limit);
46
+ } catch {
47
+ return (await Tools().list({ name: { $matchWords: query } }, { pageSize: 500 }))
48
+ .filter((row) => row.catalogState !== "quarantined")
49
+ .slice(0, limit);
50
+ }
51
+ }
@@ -1,74 +1,39 @@
1
- import { Tools } from "@peers-app/peers-sdk";
2
- import { Input } from "../../components/input";
3
1
  import { LoadingIndicator } from "../../components/loading-indicator";
4
- import { SaveButton } from "../../components/save-button";
5
2
  import { ScreenTabBody, Tabs } from "../../components/tabs";
6
3
  import { usePromise } from "../../hooks";
7
4
  import { updateActiveTabTitle } from "../../tabs-layout/tabs-state";
8
- import { ToolCode } from "./tool-code";
5
+ import { getToolDetails } from "./tool-catalog-ipc";
9
6
  import { ToolInfo } from "./tool-info";
10
7
  import { ToolSchema } from "./tool-schema";
11
8
 
12
- interface IToolDetailsProps {
13
- toolId: string;
14
- }
15
-
16
- export const ToolDetails = (props: IToolDetailsProps) => {
17
- const tool = usePromise(
9
+ /** Read-only permanent tool metadata and live provider details. */
10
+ export const ToolDetails = (props: { toolId: string }) => {
11
+ const details = usePromise(
18
12
  async () => {
19
- const toolData = await Tools().get(props.toolId);
20
- if (!toolData) return null;
21
- updateActiveTabTitle(toolData.name || "Tool");
22
- return Tools().initDoc(toolData);
13
+ const details = await getToolDetails(props.toolId);
14
+ if (details) updateActiveTabTitle(details.entry?.name || details.row.name || "Tool");
15
+ return details ?? null;
23
16
  },
24
17
  undefined,
25
18
  [props.toolId],
26
19
  );
27
-
28
- if (tool === null) {
29
- return <div>Tool not found</div>;
30
- }
31
-
32
- if (!tool) {
33
- return <LoadingIndicator />;
34
- }
35
-
36
- function saveChanges() {
37
- if (!tool) return;
38
- tool.save();
39
- }
40
-
20
+ if (details === null) return <div>Tool not found</div>;
21
+ if (!details) return <LoadingIndicator />;
22
+ const { row, entry } = details;
41
23
  return (
42
- <div className="container-fluid p-3" style={{ overflowY: "hidden" }}>
43
- <div className="d-flex">
44
- <div>
45
- <h4>
46
- <i className="bi bi-tools me-2"></i>
47
- </h4>
48
- </div>
49
- <div className="flex-grow-1">
50
- <h4>
51
- <Input
52
- key={tool.toolId}
53
- className="border border-0"
54
- style={{ width: "100%", outline: "none", backgroundColor: "transparent" }}
55
- value={tool.qs.name}
56
- />
57
- </h4>
58
- </div>
59
- <div>
60
- <SaveButton key={tool.toolId} onClick={saveChanges} doc={tool} />
61
- </div>
62
- </div>
63
-
24
+ <div className="container-fluid p-3">
25
+ <h4>
26
+ <i className="bi bi-tools me-2" />
27
+ {entry?.name ?? row.name}
28
+ </h4>
64
29
  <Tabs
65
- key={tool.toolId}
30
+ key={row.toolId}
66
31
  tabs={[
67
32
  {
68
33
  name: "Info",
69
34
  content: (
70
35
  <ScreenTabBody>
71
- <ToolInfo tool={tool} />
36
+ <ToolInfo tool={row} entry={entry} />
72
37
  </ScreenTabBody>
73
38
  ),
74
39
  },
@@ -76,24 +41,10 @@ export const ToolDetails = (props: IToolDetailsProps) => {
76
41
  name: "Schema",
77
42
  content: (
78
43
  <ScreenTabBody>
79
- <ToolSchema tool={tool} />
80
- </ScreenTabBody>
81
- ),
82
- },
83
- {
84
- name: "Code",
85
- content: (
86
- <ScreenTabBody>
87
- <ToolCode tool={tool} />
44
+ <ToolSchema tool={row} entry={entry} />
88
45
  </ScreenTabBody>
89
46
  ),
90
47
  },
91
- // {
92
- // name: 'Tests', content:
93
- // <ScreenTabBody>
94
- // <ToolTestList toolId={tool.toolId} />
95
- // </ScreenTabBody>
96
- // },
97
48
  ]}
98
49
  />
99
50
  </div>
@@ -1,59 +1,45 @@
1
- import { type IDoc, type ITool, observable } from "@peers-app/peers-sdk";
2
- import { useEffect, useState } from "react";
3
- import { AccessLevelField } from "../../components/access-level-field";
4
- import { Checkbox } from "../../components/checkbox";
5
- import { Input } from "../../components/input";
6
- import { MarkdownEditorInline } from "../../components/markdown-editor/editor-inline";
7
-
8
- /** Info tab for a tool, including its required access level. */
9
- export const ToolInfo = (props: { tool: IDoc<ITool> }) => {
10
- const { tool } = props;
11
-
12
- const [markdownUsageObs] = useState(() => observable(tool.usageDescription));
13
- const [markdownDetailedObs] = useState(() => observable(tool.detailedDescription || ""));
14
-
15
- useEffect(() => {
16
- const sub = markdownUsageObs.subscribe(() => {
17
- tool.usageDescription = markdownUsageObs();
18
- });
19
- const sub2 = tool.qs.name.subscribe(() => {
20
- tool.name = tool.qs
21
- .name()
22
- .replaceAll(/\s/g, "_")
23
- .replaceAll(/[^a-zA-Z0-9_-]/g, "");
24
- });
25
- const sub3 = markdownDetailedObs.subscribe(() => {
26
- tool.detailedDescription = markdownDetailedObs();
27
- });
28
- return () => {
29
- sub.dispose();
30
- sub2.dispose();
31
- sub3.dispose();
32
- };
33
- }, [tool, markdownDetailedObs, markdownUsageObs]);
1
+ import { getAccessLevelLabel, type ITool, type IToolCatalogEntry } from "@peers-app/peers-sdk";
2
+ import { MarkdownWithMentions } from "../../components/markdown-with-mentions";
34
3
 
4
+ /** Display permanent identity and the host’s current dispatch and access projection. */
5
+ export const ToolInfo = ({ tool, entry }: { tool: ITool; entry?: IToolCatalogEntry }) => {
6
+ const target = entry?.target;
7
+ const level = entry?.access?.accessLevel;
35
8
  return (
36
9
  <div>
37
- <small>Name:</small>
38
- <Input
39
- value={tool.qs.name}
40
- className="form-control mb-3 p-0 ps-2"
41
- placeholder="Tool name"
42
- title="Tool name"
43
- />
44
-
45
- <AccessLevelField value={tool.qs.accessLevel} />
46
-
47
- <div>
48
- <label className="form-label small mt-2 me-2">Is Assistant Runner:</label>
49
- <Checkbox className="form-check-input mt-2" checked={tool.qs.isAssistantRunner} />
50
- </div>
51
-
52
- <small>Usage Description:</small>
53
- <MarkdownEditorInline value={markdownUsageObs} />
54
- <br />
55
- <small>Detailed Description:</small>
56
- <MarkdownEditorInline value={markdownDetailedObs} />
10
+ {tool.isAssistantRunner && <span className="badge bg-secondary">Assistant runner</span>}
11
+ <dl>
12
+ <dt>ID</dt>
13
+ <dd>{tool.toolId}</dd>
14
+ <dt>Aliases</dt>
15
+ <dd>{entry?.aliases.join(", ") ?? tool.toolId}</dd>
16
+ <dt>Contract</dt>
17
+ <dd>
18
+ {target
19
+ ? `${target.contractId}@${target.version}`
20
+ : tool.contractId
21
+ ? `${tool.contractId}@${tool.contractVersion}`
22
+ : "Unresolved"}
23
+ </dd>
24
+ <dt>Member</dt>
25
+ <dd>{target?.toolName ?? tool.toolName ?? tool.name}</dd>
26
+ <dt>Provider</dt>
27
+ <dd>{entry?.provider?.activePackageId ?? "Unavailable"}</dd>
28
+ <dt>Dispatch</dt>
29
+ <dd>
30
+ {entry?.availability.dispatch ?? "Host catalog unavailable"}
31
+ {entry?.availability.message && `: ${entry.availability.message}`}
32
+ </dd>
33
+ <dt>Live access</dt>
34
+ <dd>{level === undefined ? "Unavailable" : `${getAccessLevelLabel(level)} (${level})`}</dd>
35
+ <dt>Workflow</dt>
36
+ <dd>
37
+ {entry ? (entry.workflowInvocable ? "Available" : "Excluded or unavailable") : "Unknown"}
38
+ {entry?.availability.requiresWorkflowRun && "; workflow run required"}
39
+ </dd>
40
+ </dl>
41
+ <MarkdownWithMentions content={entry?.usageDescription ?? tool.usageDescription} />
42
+ {tool.detailedDescription && <MarkdownWithMentions content={tool.detailedDescription} />}
57
43
  </div>
58
44
  );
59
45
  };
@@ -1,21 +1,13 @@
1
- import {
2
- FieldType,
3
- type ICursorIterable,
4
- IOSchemaType,
5
- type ITool,
6
- newid,
7
- observable,
8
- Tools,
9
- } from "@peers-app/peers-sdk";
10
- import type React from "react";
1
+ import { type ICursorIterable, type ITool, newid, observable, Tools } from "@peers-app/peers-sdk";
11
2
  import { useEffect, useState } from "react";
12
3
  import { Input } from "../../components/input";
13
4
  import { LazyList } from "../../components/lazy-list";
14
5
  import { LoadingIndicator } from "../../components/loading-indicator";
15
6
  import { MarkdownWithMentions } from "../../components/markdown-with-mentions";
16
- import { isDesktop, mainContentPath } from "../../globals";
7
+ import { isDesktop } from "../../globals";
17
8
  import { useObservable } from "../../hooks";
18
9
 
10
+ /** Browse permanent tool catalog rows. */
19
11
  export function ToolList() {
20
12
  const [searchTextObs] = useState(() => observable(""));
21
13
  const [searchText] = useObservable(searchTextObs);
@@ -48,30 +40,6 @@ export function ToolList() {
48
40
  return () => sub.dispose();
49
41
  }, [cursorId, cursorObs, searchTextObs.subscribe]);
50
42
 
51
- async function searchSubmit(evt: React.KeyboardEvent<HTMLInputElement>) {
52
- if (evt.key !== "Enter") return;
53
- if (!searchText.trim()) return;
54
- const toolName = searchText
55
- .trim()
56
- .replace(/\s+/g, "_")
57
- .replaceAll(/[^a-zA-Z0-9_]/g, "");
58
- const tool = await Tools().initRecord({
59
- name: toolName,
60
- usageDescription: searchText.trim(),
61
- detailedDescription: "",
62
- inputSchema: {
63
- type: IOSchemaType.simple,
64
- fields: [{ name: "input", type: FieldType.string, description: "" }],
65
- },
66
- outputSchema: {
67
- type: IOSchemaType.simple,
68
- fields: [{ name: "output", type: FieldType.string, description: "" }],
69
- },
70
- });
71
- await Tools().insert(tool);
72
- mainContentPath(`tools/${tool.toolId}`);
73
- }
74
-
75
43
  async function loadMore(existingTools: ITool[]): Promise<ITool[]> {
76
44
  const moreMatches: ITool[] = [];
77
45
  const cursor = cursorObs() || (await newCursor());
@@ -93,9 +61,8 @@ export function ToolList() {
93
61
  value={searchTextObs}
94
62
  className="form-control mt-3"
95
63
  style={{ marginBottom: "10px" }}
96
- placeholder="Search or create new tool"
64
+ placeholder="Search tools"
97
65
  autoFocus={!!isDesktop()}
98
- onKeyUp={(evt) => searchSubmit(evt)}
99
66
  />
100
67
 
101
68
  <div className="peers-list-container">