@peers-app/peers-ui 0.25.21 → 0.26.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.
Files changed (36) 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/package.json +3 -3
  23. package/src/mention-configs.ts +9 -5
  24. package/src/screens/assistants/assistant-config.test.tsx +54 -0
  25. package/src/screens/assistants/assistant-config.tsx +42 -11
  26. package/src/screens/settings/settings-page.test.tsx +24 -0
  27. package/src/screens/settings/settings-page.tsx +1 -255
  28. package/src/screens/tools/tool-catalog-ipc.test.ts +46 -0
  29. package/src/screens/tools/tool-catalog-ipc.ts +51 -0
  30. package/src/screens/tools/tool-details.tsx +18 -67
  31. package/src/screens/tools/tool-info.tsx +39 -53
  32. package/src/screens/tools/tool-list.tsx +4 -37
  33. package/src/screens/tools/tool-schema.tsx +40 -45
  34. package/dist/screens/tools/tool-code.d.ts +0 -4
  35. package/dist/screens/tools/tool-code.js +0 -28
  36. package/src/screens/tools/tool-code.tsx +0 -30
@@ -3,35 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ToolInfo = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const peers_sdk_1 = require("@peers-app/peers-sdk");
6
- const react_1 = require("react");
7
- const access_level_field_1 = require("../../components/access-level-field");
8
- const checkbox_1 = require("../../components/checkbox");
9
- const input_1 = require("../../components/input");
10
- const editor_inline_1 = require("../../components/markdown-editor/editor-inline");
11
- /** Info tab for a tool, including its required access level. */
12
- const ToolInfo = (props) => {
13
- const { tool } = props;
14
- const [markdownUsageObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(tool.usageDescription));
15
- const [markdownDetailedObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(tool.detailedDescription || ""));
16
- (0, react_1.useEffect)(() => {
17
- const sub = markdownUsageObs.subscribe(() => {
18
- tool.usageDescription = markdownUsageObs();
19
- });
20
- const sub2 = tool.qs.name.subscribe(() => {
21
- tool.name = tool.qs
22
- .name()
23
- .replaceAll(/\s/g, "_")
24
- .replaceAll(/[^a-zA-Z0-9_-]/g, "");
25
- });
26
- const sub3 = markdownDetailedObs.subscribe(() => {
27
- tool.detailedDescription = markdownDetailedObs();
28
- });
29
- return () => {
30
- sub.dispose();
31
- sub2.dispose();
32
- sub3.dispose();
33
- };
34
- }, [tool, markdownDetailedObs, markdownUsageObs]);
35
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("small", { children: "Name:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: tool.qs.name, className: "form-control mb-3 p-0 ps-2", placeholder: "Tool name", title: "Tool name" }), (0, jsx_runtime_1.jsx)(access_level_field_1.AccessLevelField, { value: tool.qs.accessLevel }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small mt-2 me-2", children: "Is Assistant Runner:" }), (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { className: "form-check-input mt-2", checked: tool.qs.isAssistantRunner })] }), (0, jsx_runtime_1.jsx)("small", { children: "Usage Description:" }), (0, jsx_runtime_1.jsx)(editor_inline_1.MarkdownEditorInline, { value: markdownUsageObs }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("small", { children: "Detailed Description:" }), (0, jsx_runtime_1.jsx)(editor_inline_1.MarkdownEditorInline, { value: markdownDetailedObs })] }));
6
+ const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
7
+ /** Display permanent identity and the host’s current dispatch and access projection. */
8
+ const ToolInfo = ({ tool, entry }) => {
9
+ const target = entry?.target;
10
+ const level = entry?.access?.accessLevel;
11
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [tool.isAssistantRunner && (0, jsx_runtime_1.jsx)("span", { className: "badge bg-secondary", children: "Assistant runner" }), (0, jsx_runtime_1.jsxs)("dl", { children: [(0, jsx_runtime_1.jsx)("dt", { children: "ID" }), (0, jsx_runtime_1.jsx)("dd", { children: tool.toolId }), (0, jsx_runtime_1.jsx)("dt", { children: "Aliases" }), (0, jsx_runtime_1.jsx)("dd", { children: entry?.aliases.join(", ") ?? tool.toolId }), (0, jsx_runtime_1.jsx)("dt", { children: "Contract" }), (0, jsx_runtime_1.jsx)("dd", { children: target
12
+ ? `${target.contractId}@${target.version}`
13
+ : tool.contractId
14
+ ? `${tool.contractId}@${tool.contractVersion}`
15
+ : "Unresolved" }), (0, jsx_runtime_1.jsx)("dt", { children: "Member" }), (0, jsx_runtime_1.jsx)("dd", { children: target?.toolName ?? tool.toolName ?? tool.name }), (0, jsx_runtime_1.jsx)("dt", { children: "Provider" }), (0, jsx_runtime_1.jsx)("dd", { children: entry?.provider?.activePackageId ?? "Unavailable" }), (0, jsx_runtime_1.jsx)("dt", { children: "Dispatch" }), (0, jsx_runtime_1.jsxs)("dd", { children: [entry?.availability.dispatch ?? "Host catalog unavailable", entry?.availability.message && `: ${entry.availability.message}`] }), (0, jsx_runtime_1.jsx)("dt", { children: "Live access" }), (0, jsx_runtime_1.jsx)("dd", { children: level === undefined ? "Unavailable" : `${(0, peers_sdk_1.getAccessLevelLabel)(level)} (${level})` }), (0, jsx_runtime_1.jsx)("dt", { children: "Workflow" }), (0, jsx_runtime_1.jsxs)("dd", { children: [entry ? (entry.workflowInvocable ? "Available" : "Excluded or unavailable") : "Unknown", entry?.availability.requiresWorkflowRun && "; workflow run required"] })] }), (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: entry?.usageDescription ?? tool.usageDescription }), tool.detailedDescription && (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: tool.detailedDescription })] }));
36
16
  };
37
17
  exports.ToolInfo = ToolInfo;
@@ -1 +1,2 @@
1
+ /** Browse permanent tool catalog rows. */
1
2
  export declare function ToolList(): import("react/jsx-runtime").JSX.Element;
@@ -10,6 +10,7 @@ const loading_indicator_1 = require("../../components/loading-indicator");
10
10
  const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
11
11
  const globals_1 = require("../../globals");
12
12
  const hooks_1 = require("../../hooks");
13
+ /** Browse permanent tool catalog rows. */
13
14
  function ToolList() {
14
15
  const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
15
16
  const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
@@ -35,31 +36,6 @@ function ToolList() {
35
36
  });
36
37
  return () => sub.dispose();
37
38
  }, [cursorId, cursorObs, searchTextObs.subscribe]);
38
- async function searchSubmit(evt) {
39
- if (evt.key !== "Enter")
40
- return;
41
- if (!searchText.trim())
42
- return;
43
- const toolName = searchText
44
- .trim()
45
- .replace(/\s+/g, "_")
46
- .replaceAll(/[^a-zA-Z0-9_]/g, "");
47
- const tool = await (0, peers_sdk_1.Tools)().initRecord({
48
- name: toolName,
49
- usageDescription: searchText.trim(),
50
- detailedDescription: "",
51
- inputSchema: {
52
- type: peers_sdk_1.IOSchemaType.simple,
53
- fields: [{ name: "input", type: peers_sdk_1.FieldType.string, description: "" }],
54
- },
55
- outputSchema: {
56
- type: peers_sdk_1.IOSchemaType.simple,
57
- fields: [{ name: "output", type: peers_sdk_1.FieldType.string, description: "" }],
58
- },
59
- });
60
- await (0, peers_sdk_1.Tools)().insert(tool);
61
- (0, globals_1.mainContentPath)(`tools/${tool.toolId}`);
62
- }
63
39
  async function loadMore(existingTools) {
64
40
  const moreMatches = [];
65
41
  const cursor = cursorObs() || (await newCursor());
@@ -77,7 +53,7 @@ function ToolList() {
77
53
  }
78
54
  return moreMatches;
79
55
  }
80
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control mt-3", style: { marginBottom: "10px" }, placeholder: "Search or create new tool", autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { loadMore: loadMore, scrollThreshold: 0.6, renderItems: (tools) => {
56
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control mt-3", style: { marginBottom: "10px" }, placeholder: "Search tools", autoFocus: !!(0, globals_1.isDesktop)() }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { loadMore: loadMore, scrollThreshold: 0.6, renderItems: (tools) => {
81
57
  return tools.map((tool) => {
82
58
  let markdownContent = `${tool.usageDescription}\n\n---\n\n${tool.detailedDescription ?? ""}`;
83
59
  const maxLen = 500;
@@ -1,4 +1,6 @@
1
- import { type IDoc, type ITool } from "@peers-app/peers-sdk";
2
- export declare const ToolSchema: (props: {
3
- tool: IDoc<ITool>;
1
+ import type { ITool, IToolCatalogEntry } from "@peers-app/peers-sdk";
2
+ /** Read-only contract field tables, plus persisted assistant runner configuration. */
3
+ export declare const ToolSchema: ({ tool, entry }: {
4
+ tool: ITool;
5
+ entry?: IToolCatalogEntry;
4
6
  }) => import("react/jsx-runtime").JSX.Element;
@@ -2,18 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ToolSchema = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
- const peers_sdk_1 = require("@peers-app/peers-sdk");
6
- const io_schema_1 = require("../../components/io-schema");
7
- const hooks_1 = require("../../hooks");
8
- const ToolSchema = (props) => {
9
- const { tool } = props;
10
- (0, hooks_1.useObservable)(tool.qs.isAssistantRunner);
11
- if (tool.isAssistantRunner && !tool.configSchema) {
12
- tool.configSchema = {
13
- type: peers_sdk_1.IOSchemaType.complex,
14
- fields: [],
15
- };
16
- }
17
- return ((0, jsx_runtime_1.jsxs)("div", { children: [tool.isAssistantRunner && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { style: { width: "130px", display: "inline-block" }, className: "fw-bold", children: "Config Schema:" }), (0, jsx_runtime_1.jsx)(io_schema_1.IOSchemaEditor, { ioSchema: tool.qs.configSchema, simpleValueName: "input" }), (0, jsx_runtime_1.jsx)("br", {})] })), (0, jsx_runtime_1.jsx)("span", { style: { width: "130px", display: "inline-block" }, className: "fw-bold", children: "Input Schema:" }), (0, jsx_runtime_1.jsx)(io_schema_1.IOSchemaEditor, { ioSchema: tool.qs.inputSchema, simpleValueName: "input" }), (0, jsx_runtime_1.jsx)("span", { style: { width: "130px", display: "inline-block" }, className: "mt-4 fw-bold", children: "Output Schema:" }), (0, jsx_runtime_1.jsx)(io_schema_1.IOSchemaEditor, { ioSchema: tool.qs.outputSchema, simpleValueName: "output" })] }));
18
- };
5
+ function FieldTable({ label, schema }) {
6
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { children: label }), !schema?.fields.length ? ((0, jsx_runtime_1.jsx)("p", { children: "No fields" })) : ((0, jsx_runtime_1.jsxs)("table", { className: "table table-sm", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Name" }), (0, jsx_runtime_1.jsx)("th", { children: "Type" }), (0, jsx_runtime_1.jsx)("th", { children: "Required" }), (0, jsx_runtime_1.jsx)("th", { children: "Description" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: schema.fields.map((field) => ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { children: field.name }), (0, jsx_runtime_1.jsxs)("td", { children: [field.type, field.isArray ? "[]" : ""] }), (0, jsx_runtime_1.jsx)("td", { children: field.optional ? "No" : "Yes" }), (0, jsx_runtime_1.jsx)("td", { children: field.description })] }, field.name))) })] }))] }));
7
+ }
8
+ /** Read-only contract field tables, plus persisted assistant runner configuration. */
9
+ const ToolSchema = ({ tool, entry }) => ((0, jsx_runtime_1.jsxs)("div", { children: [tool.isAssistantRunner && (0, jsx_runtime_1.jsx)(FieldTable, { label: "Config Schema", schema: tool.configSchema }), (0, jsx_runtime_1.jsx)(FieldTable, { label: "Input Schema", schema: entry?.inputSchema ?? tool.inputSchema }), (0, jsx_runtime_1.jsx)(FieldTable, { label: "Output Schema", schema: entry?.outputSchema ?? tool.outputSchema })] }));
19
10
  exports.ToolSchema = ToolSchema;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.25.21",
3
+ "version": "0.26.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -30,7 +30,7 @@
30
30
  "lint:fix": "biome check --write ."
31
31
  },
32
32
  "peerDependencies": {
33
- "@peers-app/peers-sdk": "^0.25.21",
33
+ "@peers-app/peers-sdk": "^0.26.1",
34
34
  "bootstrap": "^5.3.3",
35
35
  "react": "^18.0.0",
36
36
  "react-dom": "^18.0.0"
@@ -41,7 +41,7 @@
41
41
  "@babel/preset-react": "^7.24.1",
42
42
  "@babel/preset-typescript": "^7.27.1",
43
43
  "@biomejs/biome": "2.3.11",
44
- "@peers-app/peers-sdk": "0.25.21",
44
+ "@peers-app/peers-sdk": "0.26.1",
45
45
  "@testing-library/dom": "^10.4.0",
46
46
  "@testing-library/jest-dom": "^6.6.3",
47
47
  "@testing-library/react": "^16.3.0",
@@ -8,10 +8,11 @@ import {
8
8
  Workflows,
9
9
  } from "@peers-app/peers-sdk";
10
10
 
11
- // import { MENTIONS_MAX_RESULTS } from "../client/components/markdown-editor/mentions-plugin";
11
+ import { listWorkflowMentionTools } from "./screens/tools/tool-catalog-ipc";
12
12
 
13
13
  const MENTIONS_MAX_RESULTS = 10;
14
14
 
15
+ /** Editor and renderer behavior for one persisted mention kind. */
15
16
  export interface IMentionConfig {
16
17
  iconClass: string;
17
18
  styleClass?: string;
@@ -22,15 +23,13 @@ export interface IMentionConfig {
22
23
  onClick?: (data: IMentionData) => void;
23
24
  }
24
25
 
26
+ /** Discover workflow-invocable tools while keeping historical mention IDs readable. */
25
27
  export const toolMentionConfig: IMentionConfig = {
26
28
  kind: "tool",
27
29
  iconClass: "bi bi-tools",
28
30
  styleClass: "mention-tool",
29
31
  query: async (search: string) => {
30
- const results = await Tools().list(
31
- { name: { $matchWords: search } },
32
- { pageSize: MENTIONS_MAX_RESULTS },
33
- );
32
+ const results = await listWorkflowMentionTools(search, MENTIONS_MAX_RESULTS);
34
33
  return results.map((p) => ({
35
34
  kind: "tool",
36
35
  id: p.toolId,
@@ -50,6 +49,7 @@ export const toolMentionConfig: IMentionConfig = {
50
49
  ),
51
50
  };
52
51
 
52
+ /** Discover users and resolve persisted user mentions. */
53
53
  export const userMentionConfig: IMentionConfig = {
54
54
  kind: "user",
55
55
  iconClass: "bi-person-fill",
@@ -78,6 +78,7 @@ export const userMentionConfig: IMentionConfig = {
78
78
  ),
79
79
  };
80
80
 
81
+ /** Discover assistants and resolve persisted assistant mentions. */
81
82
  export const assistantMentionConfig: IMentionConfig = {
82
83
  kind: "assistant",
83
84
  iconClass: "bi bi-person-fill-gear",
@@ -106,6 +107,7 @@ export const assistantMentionConfig: IMentionConfig = {
106
107
  ),
107
108
  };
108
109
 
110
+ /** Discover workflows and resolve persisted workflow mentions. */
109
111
  export const workflowMentionConfig: IMentionConfig = {
110
112
  kind: "workflow",
111
113
  iconClass: "bi bi-database-fill-gear",
@@ -134,6 +136,7 @@ export const workflowMentionConfig: IMentionConfig = {
134
136
  ),
135
137
  };
136
138
 
139
+ /** Discover data types and navigate to their definitions. */
137
140
  export const valueTypeMentionConfig: IMentionConfig = {
138
141
  kind: "type",
139
142
  iconClass: "bi bi-code-square",
@@ -172,6 +175,7 @@ export const valueTypeMentionConfig: IMentionConfig = {
172
175
  // }))),
173
176
  // picture = <i className="bi bi-list-task pe-1" />;
174
177
 
178
+ /** Shared mention definitions used by editors and rendered messages. */
175
179
  export const mentionConfigs = [
176
180
  toolMentionConfig,
177
181
  userMentionConfig,
@@ -0,0 +1,54 @@
1
+ import { type IAssistant, type IDoc, observable } from "@peers-app/peers-sdk";
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import { AssistantConfig } from "./assistant-config";
4
+
5
+ const mockRunners = [
6
+ { toolId: "selected", name: "Retired", catalogState: "quarantined" },
7
+ { toolId: "hidden", name: "Other unavailable", catalogState: "quarantined" },
8
+ { toolId: "available", name: "Available" },
9
+ ];
10
+ const mockCatalog = jest.fn();
11
+ jest.mock("@peers-app/peers-sdk", () => ({
12
+ ...jest.requireActual("@peers-app/peers-sdk"),
13
+ Tools: () => ({
14
+ get: async () => mockRunners[0],
15
+ list: async () => mockRunners,
16
+ }),
17
+ }));
18
+ jest.mock("../tools/tool-catalog-ipc", () => ({
19
+ getToolCatalogConsumer: () => mockCatalog(),
20
+ }));
21
+ jest.mock("../../components/io-schema-values", () => ({ IOSchemaValues: () => null }));
22
+
23
+ test.each([
24
+ "catalog",
25
+ "browser fallback",
26
+ ])("runner selection retains a disabled current runner in %s", async (mode) => {
27
+ mockCatalog.mockReset();
28
+ if (mode === "catalog")
29
+ mockCatalog.mockResolvedValue({
30
+ tools: {
31
+ getTool: async ({ toolIdOrName }: { toolIdOrName: string }) => ({
32
+ tool: { workflowInvocable: toolIdOrName === "available" },
33
+ }),
34
+ },
35
+ });
36
+ else mockCatalog.mockRejectedValue(new Error("No host transport"));
37
+ const selected = observable("selected");
38
+ const assistant = {
39
+ assistantRunnerToolId: "selected",
40
+ qs: { assistantRunnerToolId: selected },
41
+ } as unknown as IDoc<IAssistant>;
42
+ const { container } = render(<AssistantConfig assistant={assistant} />);
43
+ await screen.findByRole("button", { name: "Available" });
44
+ const retired = container.querySelector<HTMLButtonElement>(".dropdown-menu button:disabled");
45
+ expect(retired).toHaveTextContent("Retired (unavailable)");
46
+ expect(container.querySelector(".dropdown-toggle")).toHaveTextContent("Retired (unavailable)");
47
+ expect(screen.queryByText("Other unavailable")).not.toBeInTheDocument();
48
+ if (!retired) throw new Error("Missing unavailable runner option");
49
+ fireEvent.click(retired);
50
+ expect(selected()).toBe("selected");
51
+ fireEvent.click(screen.getByRole("button", { name: "Available" }));
52
+ await waitFor(() => expect(selected()).toBe("available"));
53
+ expect(screen.queryByText("Retired (unavailable)")).not.toBeInTheDocument();
54
+ });
@@ -8,7 +8,9 @@ import {
8
8
  import { IOSchemaValues } from "../../components/io-schema-values";
9
9
  import { LoadingIndicator } from "../../components/loading-indicator";
10
10
  import { useObservable, usePromise } from "../../hooks";
11
+ import { getToolCatalogConsumer } from "../tools/tool-catalog-ipc";
11
12
 
13
+ /** Edit assistant runner selection and its configuration. */
12
14
  export const AssistantConfig = (props: { assistant: IDoc<IAssistant> }) => {
13
15
  const { assistant } = props;
14
16
 
@@ -57,7 +59,25 @@ const AssistantRunnersDropdown = (props: { selectedToolId: Observable<string> })
57
59
 
58
60
  const modelTools = usePromise(async () => {
59
61
  const modelTools = await Tools().list({ isAssistantRunner: true });
60
- return modelTools;
62
+ let availableIds: Set<string>;
63
+ try {
64
+ const catalog = await getToolCatalogConsumer();
65
+ const entries = await Promise.all(
66
+ modelTools.map((tool) =>
67
+ catalog.tools.getTool({ toolIdOrName: tool.toolId, runnableOnly: true }),
68
+ ),
69
+ );
70
+ availableIds = new Set(
71
+ modelTools
72
+ .filter((_tool, index) => entries[index].tool?.workflowInvocable)
73
+ .map((tool) => tool.toolId),
74
+ );
75
+ } catch {
76
+ availableIds = new Set(
77
+ modelTools.filter((tool) => tool.catalogState !== "quarantined").map((tool) => tool.toolId),
78
+ );
79
+ }
80
+ return modelTools.map((tool) => ({ tool, available: availableIds.has(tool.toolId) }));
61
81
  });
62
82
 
63
83
  if (!modelTools) {
@@ -72,18 +92,29 @@ const AssistantRunnersDropdown = (props: { selectedToolId: Observable<string> })
72
92
  data-bs-toggle="dropdown"
73
93
  aria-expanded="false"
74
94
  >
75
- {modelTools.find((tool) => tool.toolId === selectedToolId)?.name ?? "Select Tool"}
95
+ {modelTools.find(({ tool }) => tool.toolId === selectedToolId)?.tool.name ?? "Select Tool"}
96
+ {modelTools.some(({ tool, available }) => tool.toolId === selectedToolId && !available)
97
+ ? " (unavailable)"
98
+ : ""}
76
99
  </button>
77
100
  <ul className="dropdown-menu">
78
- {modelTools.map((tool) => {
79
- return (
80
- <li key={tool.toolId} onClick={() => props.selectedToolId(tool.toolId)}>
81
- {/* <button className="dropdown-item" type="button">Action</button> */}
82
- {/* <MarkdownWithMentions content={tool.name} /> */}
83
- <span className="dropdown-item">{tool.name}</span>
84
- </li>
85
- );
86
- })}
101
+ {modelTools
102
+ .filter(({ tool, available }) => available || tool.toolId === selectedToolId)
103
+ .map(({ tool, available }) => {
104
+ return (
105
+ <li key={tool.toolId}>
106
+ <button
107
+ className="dropdown-item"
108
+ type="button"
109
+ disabled={!available}
110
+ onClick={() => props.selectedToolId(tool.toolId)}
111
+ >
112
+ {tool.name}
113
+ {available ? "" : " (unavailable)"}
114
+ </button>
115
+ </li>
116
+ );
117
+ })}
87
118
  </ul>
88
119
  </div>
89
120
  );
@@ -0,0 +1,24 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { SettingsPage } from "./settings-page";
3
+
4
+ jest.mock("../../components/tooltip", () => ({ Tooltip: () => null }));
5
+ jest.mock("../../hooks", () => ({ usePromise: () => undefined, useObservable: () => [false] }));
6
+ jest.mock("../../components/tabs", () => ({
7
+ Tabs: ({ tabs }: any) => (
8
+ <>
9
+ {tabs.map((tab: any) => (
10
+ <div key={tab.name}>{tab.content}</div>
11
+ ))}
12
+ </>
13
+ ),
14
+ }));
15
+ jest.mock("../../root-layout/layout-picker", () => ({ LayoutPicker: () => <div>Layout</div> }));
16
+ jest.mock("./color-mode-dropdown", () => ({ ColorModeDropdown: () => <div>Theme</div> }));
17
+ jest.mock("../../globals", () => ({ me: {}, isDesktop: () => false }));
18
+
19
+ test("Settings renders advanced controls without the retired Tasks importer", () => {
20
+ const html = renderToStaticMarkup(<SettingsPage />);
21
+ expect(html).toContain("Advanced Settings");
22
+ expect(html).not.toContain("Import Old Peers");
23
+ expect(html).not.toContain("Dry Run");
24
+ });