@anchrd/intel 0.59.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.
- package/README.md +496 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.js +416 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +128 -0
- package/dist/adapters/content/content.types.d.ts +38 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-audit.d.ts +62 -0
- package/dist/adapters/db/db-audit.js +150 -0
- package/dist/adapters/db/db-boards.d.ts +6 -0
- package/dist/adapters/db/db-boards.js +324 -0
- package/dist/adapters/db/db-feed.d.ts +30 -0
- package/dist/adapters/db/db-feed.js +276 -0
- package/dist/adapters/db/db-flows.d.ts +42 -0
- package/dist/adapters/db/db-flows.js +1311 -0
- package/dist/adapters/db/db-grants.d.ts +133 -0
- package/dist/adapters/db/db-grants.js +269 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +200 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-prompts.d.ts +46 -0
- package/dist/adapters/db/db-prompts.js +144 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +1687 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +26 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +77 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +301 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +129 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +139 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +87 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/audit/audit.d.ts +2 -0
- package/dist/audit/audit.js +87 -0
- package/dist/audit/audit.types.d.ts +23 -0
- package/dist/audit/audit.types.js +1 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +284 -0
- package/dist/auth/auth.types.d.ts +108 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/boards/boards.d.ts +44 -0
- package/dist/boards/boards.js +447 -0
- package/dist/boards/boards.types.d.ts +117 -0
- package/dist/boards/boards.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +192 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1194 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +262 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/contract/audit.d.ts +50 -0
- package/dist/contract/audit.js +72 -0
- package/dist/contract/board.d.ts +150 -0
- package/dist/contract/board.js +262 -0
- package/dist/contract/bundle.d.ts +147 -0
- package/dist/contract/bundle.js +139 -0
- package/dist/contract/contract.d.ts +112 -0
- package/dist/contract/contract.js +143 -0
- package/dist/contract/feed.d.ts +126 -0
- package/dist/contract/feed.js +149 -0
- package/dist/contract/flow-run.d.ts +346 -0
- package/dist/contract/flow-run.js +194 -0
- package/dist/contract/flow.d.ts +1057 -0
- package/dist/contract/flow.js +552 -0
- package/dist/contract/node.d.ts +551 -0
- package/dist/contract/node.js +456 -0
- package/dist/contract/share.d.ts +216 -0
- package/dist/contract/share.js +134 -0
- package/dist/contract/table.d.ts +171 -0
- package/dist/contract/table.js +117 -0
- package/dist/contract/tool.d.ts +122 -0
- package/dist/contract/tool.js +172 -0
- package/dist/feed/feed.d.ts +2 -0
- package/dist/feed/feed.js +77 -0
- package/dist/feed/feed.types.d.ts +23 -0
- package/dist/feed/feed.types.js +1 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +2216 -0
- package/dist/flows/flows.types.d.ts +339 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +9 -0
- package/dist/http/http.js +874 -0
- package/dist/http/http.types.d.ts +22 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +239 -0
- package/dist/indexing/indexing.types.d.ts +11 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +194 -0
- package/dist/intel/intel.types.d.ts +24 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +1292 -0
- package/dist/mcp/mcp.types.d.ts +25 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/nodes/document-links/document-links.d.ts +20 -0
- package/dist/nodes/document-links/document-links.js +57 -0
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1667 -0
- package/dist/nodes/nodes.types.d.ts +511 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/prompts/prompts.d.ts +2 -0
- package/dist/prompts/prompts.js +65 -0
- package/dist/prompts/prompts.types.d.ts +71 -0
- package/dist/prompts/prompts.types.js +1 -0
- package/dist/shared/callers/callers.d.ts +28 -0
- package/dist/shared/callers/callers.js +30 -0
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/document-text/document-text.d.ts +21 -0
- package/dist/shared/document-text/document-text.js +31 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
- package/dist/shared/gate-authorization/gate-authorization.js +26 -0
- package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
- package/dist/shared/grant-expiry/grant-expiry.js +26 -0
- package/dist/shared/intel-error/intel-error.d.ts +22 -0
- package/dist/shared/intel-error/intel-error.js +27 -0
- package/dist/shared/plain-title/plain-title.d.ts +16 -0
- package/dist/shared/plain-title/plain-title.js +45 -0
- package/dist/shared/problem-details/problem-details.d.ts +30 -0
- package/dist/shared/problem-details/problem-details.js +18 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +2 -0
- package/dist/shared/sha256/sha256.js +14 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +177 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +207 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/migrations/0007_no_node_waits.sql +69 -0
- package/migrations/0008_three_layers.sql +130 -0
- package/migrations/0009_no_context_policy.sql +48 -0
- package/migrations/0010_no_run_waits_a_year.sql +27 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +97 -0
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/migrations/0020_cascade_purge_replay.sql +11 -0
- package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
- package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
- package/migrations/0023_a_board_and_its_tasks.sql +116 -0
- package/migrations/0024_the_archive_is_a_column.sql +33 -0
- package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
- package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
- package/migrations/0027_the_runs_of_every_flow.sql +27 -0
- package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
- package/package.json +118 -0
- package/src/contract/CLAUDE.md +152 -0
- package/src/contract/README.md +102 -0
- package/src/contract/audit.ts +85 -0
- package/src/contract/board.ts +306 -0
- package/src/contract/bundle.ts +157 -0
- package/src/contract/contract.ts +161 -0
- package/src/contract/feed.ts +165 -0
- package/src/contract/flow-run.ts +241 -0
- package/src/contract/flow.ts +675 -0
- package/src/contract/node.ts +583 -0
- package/src/contract/share.ts +196 -0
- package/src/contract/table.ts +178 -0
- package/src/contract/tool.ts +202 -0
- package/ui/README.md +110 -0
- package/ui/components.json +21 -0
- package/ui/index.html +13 -0
- package/ui/package.json +11 -0
- package/ui/scripts/lint-tokens.mjs +187 -0
- package/ui/src/access-summary/access-summary.tsx +385 -0
- package/ui/src/app/action-slot/action-slot.tsx +27 -0
- package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
- package/ui/src/app/app-tree/app-tree.tsx +1175 -0
- package/ui/src/app/app.tsx +87 -0
- package/ui/src/app/header-search/header-search.tsx +294 -0
- package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
- package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
- package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
- package/ui/src/app/tree-move/tree-move.tsx +427 -0
- package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
- package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
- package/ui/src/app/user-footer/user-footer.tsx +188 -0
- package/ui/src/app/view-menu/view-menu.tsx +115 -0
- package/ui/src/app-root/app-root.tsx +109 -0
- package/ui/src/app-root/app-root.types.ts +27 -0
- package/ui/src/archive/archive.tsx +417 -0
- package/ui/src/attachment-detail/attachment-detail.ts +52 -0
- package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
- package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
- package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
- package/ui/src/board/board-assignee/board-assignee.ts +48 -0
- package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
- package/ui/src/board/board-chip/board-chip.tsx +46 -0
- package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
- package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
- package/ui/src/board/board-data/board-data.ts +195 -0
- package/ui/src/board/board-data/board-data.types.ts +38 -0
- package/ui/src/board/board-dates/board-dates.ts +40 -0
- package/ui/src/board/board-dates/board-dates.tsx +105 -0
- package/ui/src/board/board-kanban/board-kanban.ts +162 -0
- package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
- package/ui/src/board/board-open-task/board-open-task.ts +32 -0
- package/ui/src/board/board-panel/board-panel.tsx +259 -0
- package/ui/src/board/board-settings/board-settings.tsx +226 -0
- package/ui/src/board/board-status/board-status.tsx +60 -0
- package/ui/src/board/board-stripes/board-stripes.ts +128 -0
- package/ui/src/board/board-table/board-table.ts +53 -0
- package/ui/src/board/board-table/board-table.tsx +610 -0
- package/ui/src/board/board-task/board-task.ts +108 -0
- package/ui/src/board/board-task/board-task.tsx +335 -0
- package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
- package/ui/src/branding/branding.tsx +17 -0
- package/ui/src/branding/custom-logo.ts +1 -0
- package/ui/src/branding/favicon.default.svg +5 -0
- package/ui/src/branding/favicon.svg +5 -0
- package/ui/src/components/ui/avatar.tsx +39 -0
- package/ui/src/components/ui/button.tsx +64 -0
- package/ui/src/components/ui/collapsible.tsx +20 -0
- package/ui/src/components/ui/command.tsx +160 -0
- package/ui/src/components/ui/dialog.tsx +143 -0
- package/ui/src/components/ui/dropdown-menu.tsx +187 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/popover.tsx +41 -0
- package/ui/src/components/ui/select.tsx +163 -0
- package/ui/src/components/ui/separator.tsx +26 -0
- package/ui/src/components/ui/sheet.tsx +136 -0
- package/ui/src/components/ui/sidebar.tsx +693 -0
- package/ui/src/components/ui/skeleton.tsx +13 -0
- package/ui/src/components/ui/switch.tsx +25 -0
- package/ui/src/components/ui/table.tsx +82 -0
- package/ui/src/components/ui/tabs.tsx +52 -0
- package/ui/src/components/ui/tooltip.tsx +51 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
- package/ui/src/data/query-client.ts +18 -0
- package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
- package/ui/src/data/request-refusal/request-refusal.ts +51 -0
- package/ui/src/data/sign-in/sign-in.ts +97 -0
- package/ui/src/data/sign-in/sign-in.types.ts +15 -0
- package/ui/src/document-link/document-link.tsx +114 -0
- package/ui/src/document-markdown/document-markdown.ts +195 -0
- package/ui/src/editor-schema/editor-schema.ts +23 -0
- package/ui/src/entry-picker/entry-picker.tsx +238 -0
- package/ui/src/feed/feed.tsx +344 -0
- package/ui/src/file-preview/file-preview-view.tsx +496 -0
- package/ui/src/file-preview/file-preview.tsx +28 -0
- package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
- package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
- package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
- package/ui/src/file-preview/word-file-preview.tsx +5 -0
- package/ui/src/flow-runs/flow-runs.tsx +238 -0
- package/ui/src/flows/flows.tsx +1646 -0
- package/ui/src/flows/node-icon/node-icon.ts +34 -0
- package/ui/src/flows/node-palette/node-palette.tsx +214 -0
- package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
- package/ui/src/folder-contents/folder-contents.tsx +321 -0
- package/ui/src/folder-index/folder-index.ts +55 -0
- package/ui/src/frontmatter/frontmatter.tsx +193 -0
- package/ui/src/graph-pane/graph-pane.tsx +53 -0
- package/ui/src/hooks/use-mobile.ts +19 -0
- package/ui/src/i18n/custom.json +4 -0
- package/ui/src/i18n/de.json +609 -0
- package/ui/src/i18n/en.json +609 -0
- package/ui/src/i18n/es.json +609 -0
- package/ui/src/i18n/i18n-context.tsx +65 -0
- package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/ui/src/i18n/i18n.ts +32 -0
- package/ui/src/i18n/i18n.types.ts +49 -0
- package/ui/src/kind-icon.ts +97 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +68 -0
- package/ui/src/modal/modal.tsx +53 -0
- package/ui/src/node-details/node-details.tsx +545 -0
- package/ui/src/node-editor/node-editor.tsx +304 -0
- package/ui/src/node-graph/graph-notice.tsx +21 -0
- package/ui/src/node-graph/node-graph.ts +214 -0
- package/ui/src/node-graph/node-graph.tsx +295 -0
- package/ui/src/node-import/node-import.tsx +418 -0
- package/ui/src/node-save-error.ts +46 -0
- package/ui/src/node-source/node-source.tsx +128 -0
- package/ui/src/node-table/node-table.tsx +107 -0
- package/ui/src/nodes/nodes.tsx +240 -0
- package/ui/src/rename-resource/rename-resource.ts +80 -0
- package/ui/src/resolved-names/resolved-names.tsx +67 -0
- package/ui/src/resource-error.ts +33 -0
- package/ui/src/resource-menu/resource-menu.tsx +1156 -0
- package/ui/src/router/router-context.ts +6 -0
- package/ui/src/router/router.tsx +67 -0
- package/ui/src/router/router.types.ts +8 -0
- package/ui/src/router/selection-search.ts +88 -0
- package/ui/src/save-button/save-button.tsx +94 -0
- package/ui/src/section-hint/section-hint.tsx +40 -0
- package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
- package/ui/src/styles.css +189 -0
- package/ui/src/table-columns/table-columns.tsx +379 -0
- package/ui/src/theme/custom.css +1 -0
- package/ui/src/theme/theme-context.tsx +89 -0
- package/ui/src/theme/theme.ts +70 -0
- package/ui/src/time/relative-time.tsx +41 -0
- package/ui/src/time/time-context.tsx +78 -0
- package/ui/src/time/time.ts +165 -0
- package/ui/src/title-row/title-row.tsx +185 -0
- package/ui/src/tools/tools.tsx +546 -0
- package/ui/src/user-name/user-name.ts +61 -0
- package/ui/src/verbatim/verbatim.tsx +68 -0
- package/ui/tsconfig.json +17 -0
- package/ui/vite.config.ts +129 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { serverOf, type ToolCapability, type ToolServer } from "@contract/tool.ts";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useRouterState } from "@tanstack/react-router";
|
|
4
|
+
import {
|
|
5
|
+
AlertTriangle,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
HelpCircle,
|
|
8
|
+
PlugZap,
|
|
9
|
+
ShieldOff,
|
|
10
|
+
Timer,
|
|
11
|
+
Wrench,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
16
|
+
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
17
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
18
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
19
|
+
import { selectedFrom } from "@/router/selection-search.ts";
|
|
20
|
+
|
|
21
|
+
interface ToolGroup {
|
|
22
|
+
// The handle the portal named, or `null` for the box holding what no named server carries.
|
|
23
|
+
handle: string | null;
|
|
24
|
+
label: string;
|
|
25
|
+
items: ToolCapability[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The tools of one catalog, under the servers the portal itself named (#212).
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ The grouping comes from `GET /api/v1/tools/servers`, never from the tool name alone. The name
|
|
32
|
+
* cannot say where the server part ends — `intel_flow_get` reads as `intel` + `flow_get` and as
|
|
33
|
+
* `intel_flow` + `get` — so this screen matches the portal's own handles with the same `serverOf`
|
|
34
|
+
* the delegation is cut with, longest match wins. The earlier reading, a cut at a separator, was
|
|
35
|
+
* shipped twice and was wrong twice: at `__` it put all 92 tools of the running installation in one
|
|
36
|
+
* box, because the portal writes a single underscore (#106), and at `_` it would break the first
|
|
37
|
+
* server whose slug carries one (#107, closed for that reason).
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ A tool that matches no named handle is NOT dropped. It goes into its own box: the portal's
|
|
40
|
+
* management tools (`portal_list_servers`) live there, because the portal does not list itself as
|
|
41
|
+
* an upstream, and so does anything from a server the directory failed to mention. Hiding those
|
|
42
|
+
* would make the screen quietly disagree with `tools/list` — and the count on the lid is what the
|
|
43
|
+
* reader compares against the portal.
|
|
44
|
+
*
|
|
45
|
+
* The order is the portal's own: a group appears where its first tool appears, because the portal
|
|
46
|
+
* returns a server's tools together and sorting would replace its answer with a preference. Only
|
|
47
|
+
* the unattributed box is forced last — it is a remainder, not a server, and it must not lead.
|
|
48
|
+
*/
|
|
49
|
+
function groupByServer(
|
|
50
|
+
items: ToolCapability[],
|
|
51
|
+
servers: ToolServer[],
|
|
52
|
+
unlistedLabel: string,
|
|
53
|
+
): ToolGroup[] {
|
|
54
|
+
const named = new Map(servers.map((server) => [server.handle, server.name]));
|
|
55
|
+
const groups = new Map<string, ToolGroup>();
|
|
56
|
+
const unlisted: ToolGroup = { handle: null, label: unlistedLabel, items: [] };
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
const handle = serverOf(item.name, named.keys());
|
|
59
|
+
if (handle === null) {
|
|
60
|
+
unlisted.items.push(item);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// The server's display name, not its handle: `notion` is what the wire says, and what the
|
|
64
|
+
// portal calls it is what the reader knows it by.
|
|
65
|
+
const group = groups.get(handle) ?? { handle, label: named.get(handle) ?? handle, items: [] };
|
|
66
|
+
group.items.push(item);
|
|
67
|
+
groups.set(handle, group);
|
|
68
|
+
}
|
|
69
|
+
return unlisted.items.length > 0 ? [...groups.values(), unlisted] : [...groups.values()];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Signed in to Intel is signed in to the portal, so the sign-in is attempted at most once per
|
|
73
|
+
// visit. A second attempt after a refusal would be a redirect loop with an unchanging answer, and
|
|
74
|
+
// the marker in the URL only survives until the next navigation — the browser session remembers it
|
|
75
|
+
// instead (#60).
|
|
76
|
+
//
|
|
77
|
+
// ⚠️ It records that an attempt was STARTED, never that one was answered (#426). A sign-in that
|
|
78
|
+
// crashed on the way leaves exactly the same note as one that came back, so nothing may read it as
|
|
79
|
+
// a verdict about anybody: it only keeps the screen from walking in a circle on its own, and what
|
|
80
|
+
// the reader is told comes from the marker in the URL instead. That is the whole distinction — the
|
|
81
|
+
// note is never cleared to make a second attempt possible; the button is what makes one possible.
|
|
82
|
+
const SignInAttemptKey = "intel.portal-sign-in-attempted";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What came back from the silent walk — and "nothing" is one of the answers (#426).
|
|
86
|
+
*
|
|
87
|
+
* The marker in the URL is the only thing that ever says why the walk ended, and it is absent in two
|
|
88
|
+
* situations that look identical from here: the attempt never ran, or it broke before it could
|
|
89
|
+
* return. Neither is a decision about access, so neither may borrow the sentence that describes one
|
|
90
|
+
* — the rule `packages/intel/ui/CLAUDE.md` keeps from #350: **a missing statement is a reason to say less,
|
|
91
|
+
* never to refuse more.** Saying "no access" for `none` sent a reader hunting through portal
|
|
92
|
+
* policies while the sign-in had answered 500 (#425).
|
|
93
|
+
*/
|
|
94
|
+
type PortalAnswer = "refused" | "expired" | "failed" | "none";
|
|
95
|
+
|
|
96
|
+
function portalAnswerOf(connectError: string | null): PortalAnswer {
|
|
97
|
+
if (connectError === null) return "none";
|
|
98
|
+
if (connectError === "portal_sign_in_expired") return "expired";
|
|
99
|
+
// The two codes Intel itself writes when somebody was actually turned away — by the portal, or by
|
|
100
|
+
// Gate before the portal was ever asked. Every other marker is machinery, not policy.
|
|
101
|
+
return connectError === "portal_sign_in_refused" || connectError === "permission_required"
|
|
102
|
+
? "refused"
|
|
103
|
+
: "failed";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Reading `sessionStorage` throws outright in a few privacy modes, so the guard is a try, not a
|
|
107
|
+
// feature check. Losing the note costs one extra redirect, and the marker the refusal leaves in the
|
|
108
|
+
// URL still ends the walk — it must never cost the screen.
|
|
109
|
+
function attemptStore(): Storage | null {
|
|
110
|
+
try {
|
|
111
|
+
return typeof window === "undefined" ? null : window.sessionStorage;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The catalog is one live `tools/list` with the signed-in user's own portal token. Nothing here is
|
|
118
|
+
// stored, mirrored or administered — the screen shows what the portal answers and nothing else.
|
|
119
|
+
export function Tools() {
|
|
120
|
+
const { data } = useIntelRouterContext();
|
|
121
|
+
const i18n = useI18n();
|
|
122
|
+
const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
|
|
123
|
+
// The servers behind that same live list (D30). Its own query, and its own failure: a portal that
|
|
124
|
+
// answers tools but no directory still has a usable screen, it just has no boxes to put them in.
|
|
125
|
+
// ⚠️ No retry on this one, and that is a decision about the screen rather than about the request.
|
|
126
|
+
// The tools are held back until the directory has answered, so every further attempt is time
|
|
127
|
+
// somebody spends in front of the word "Loading" — and a retry does not even have to happen: a
|
|
128
|
+
// React Query retry is PAUSED while the tab is unfocused, so a screen waiting on one can stay
|
|
129
|
+
// "Loading" for as long as the reader is looking elsewhere. A portal that offers no directory
|
|
130
|
+
// answers the same 502 every time anyway; one attempt settles it, and what follows is the list
|
|
131
|
+
// plus the sentence saying why it is not sorted.
|
|
132
|
+
const servers = useQuery({
|
|
133
|
+
queryKey: ["tool-servers"],
|
|
134
|
+
queryFn: () => data.listToolServers(),
|
|
135
|
+
retry: false,
|
|
136
|
+
});
|
|
137
|
+
// A hit from the header search arrives as `?select=<tool name>`, and the row it names opens with
|
|
138
|
+
// the list. Nothing else about the row changes: it is still only a disclosure.
|
|
139
|
+
const requested = useRouterState({ select: (state) => selectedFrom(state.location.search) });
|
|
140
|
+
// The silent sign-in returns here with a marker when it failed. The value comes from outside
|
|
141
|
+
// and could carry provider detail, so it is never rendered — it is only compared against the
|
|
142
|
+
// codes Intel itself writes, to tell "you may not" from "the sign-in broke" (#93): the first is
|
|
143
|
+
// a sentence about access, the second must not masquerade as one.
|
|
144
|
+
const connectError =
|
|
145
|
+
typeof window === "undefined"
|
|
146
|
+
? null
|
|
147
|
+
: new URLSearchParams(window.location.search).get("connectError");
|
|
148
|
+
const answer = portalAnswerOf(connectError);
|
|
149
|
+
// What the screen DID about that answer, which is a third thing beside the catalog and the marker.
|
|
150
|
+
//
|
|
151
|
+
// ⚠️ Only `none` starts undecided. `useEffect` runs after the paint, so a state that always began
|
|
152
|
+
// at `deciding` would push a "Loading" frame in front of all four notices — and three of them are
|
|
153
|
+
// already settled at the first render, because their answer stands in the URL. It would flash at
|
|
154
|
+
// exactly the readers who just arrived out of a redirect. `none` is the one that genuinely has to
|
|
155
|
+
// wait: its walk starts one tick after the catalog arrives, and "we do not know" in between is a
|
|
156
|
+
// sentence the screen takes back immediately.
|
|
157
|
+
const [walk, setWalk] = React.useState<"deciding" | "running" | "stopped">(
|
|
158
|
+
answer === "none" ? "deciding" : "stopped",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const startSignIn = React.useCallback(() => {
|
|
162
|
+
// ⚠️ `setItem`, and the marker is not cleared anywhere on this path. Pressing the button STARTS
|
|
163
|
+
// an attempt, which is the only thing the marker ever claims — clearing it would buy the click
|
|
164
|
+
// nothing (no code in this path reads it) and cost the next visit: an attempt that dies without
|
|
165
|
+
// ever coming back with a marker would be followed by a silent redirect nobody asked for, on a
|
|
166
|
+
// screen that has just said it does not understand what is happening. The second attempt this
|
|
167
|
+
// browser session owes its reader is this button, never a re-armed guard.
|
|
168
|
+
attemptStore()?.setItem(SignInAttemptKey, "1");
|
|
169
|
+
setWalk("running");
|
|
170
|
+
data.startPortalSignIn("/tools");
|
|
171
|
+
}, [data]);
|
|
172
|
+
|
|
173
|
+
// ⚠️ Nobody is asked to start this. The portal sign-in is a browser redirect, so the screen
|
|
174
|
+
// begins it itself the moment the catalog says there is no portal session yet — that is the whole
|
|
175
|
+
// of what replaced the "Connect the portal" button.
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
if (!catalog.data) return;
|
|
178
|
+
const attempts = attemptStore();
|
|
179
|
+
if (catalog.data.portalConnected) {
|
|
180
|
+
// An answered sign-in frees the next one: when the token later expires beyond renewal, the
|
|
181
|
+
// same silent walk runs again rather than stopping at a screen.
|
|
182
|
+
attempts?.removeItem(SignInAttemptKey);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (answer !== "none" || attempts?.getItem(SignInAttemptKey)) {
|
|
186
|
+
setWalk("stopped");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
attempts?.setItem(SignInAttemptKey, "1");
|
|
190
|
+
setWalk("running");
|
|
191
|
+
data.startPortalSignIn("/tools");
|
|
192
|
+
}, [catalog.data, data, answer]);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div className="min-h-full p-8">
|
|
196
|
+
<div className="mx-auto w-full max-w-4xl space-y-5">
|
|
197
|
+
{catalog.isPending ? (
|
|
198
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
199
|
+
) : catalog.isError ? (
|
|
200
|
+
// Unreachable is not empty: the portal did not answer, so Intel knows nothing about this
|
|
201
|
+
// user's tools rather than knowing there are none.
|
|
202
|
+
<Notice
|
|
203
|
+
alert
|
|
204
|
+
icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-destructive" />}
|
|
205
|
+
title={i18n.t("tools.unreachable")}
|
|
206
|
+
help={i18n.t("tools.unreachableHelp")}
|
|
207
|
+
>
|
|
208
|
+
<Button variant="outline" onClick={() => void catalog.refetch()}>
|
|
209
|
+
{i18n.t("common.retry")}
|
|
210
|
+
</Button>
|
|
211
|
+
</Notice>
|
|
212
|
+
) : !catalog.data.portalConnected ? (
|
|
213
|
+
walk === "deciding" ? (
|
|
214
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
215
|
+
) : walk === "running" ? (
|
|
216
|
+
<p className="text-sm text-muted-foreground">{i18n.t("tools.signingIn")}</p>
|
|
217
|
+
) : (
|
|
218
|
+
<PortalNotice answer={answer} i18n={i18n} onRetry={startSignIn} />
|
|
219
|
+
)
|
|
220
|
+
) : catalog.data.items.length === 0 ? (
|
|
221
|
+
<Notice
|
|
222
|
+
icon={<Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
223
|
+
title={i18n.t("tools.empty")}
|
|
224
|
+
help={i18n.t("tools.emptyHelp")}
|
|
225
|
+
/>
|
|
226
|
+
) : servers.isPending ? (
|
|
227
|
+
// The tools are here and the servers are not yet. Showing the flat list first and
|
|
228
|
+
// reshuffling it a moment later would be a screen that moves under the reader's hand.
|
|
229
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
230
|
+
) : (
|
|
231
|
+
<ToolCatalogView
|
|
232
|
+
items={catalog.data.items}
|
|
233
|
+
servers={servers.isError ? [] : servers.data.items}
|
|
234
|
+
i18n={i18n}
|
|
235
|
+
requested={requested}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The end of the silent walk, in the words of whatever ended it — one sentence per answer, and no
|
|
245
|
+
* sentence shared between two of them (#426).
|
|
246
|
+
*
|
|
247
|
+
* ⚠️ Only `refused` is a statement about ACCESS, and only it is a dead end: there is nothing the
|
|
248
|
+
* reader could press that would change the answer, so nothing is offered. The other two are the
|
|
249
|
+
* machinery and the silence, and both are worth another attempt — which is the button the screen
|
|
250
|
+
* lacked entirely while the marker in `sessionStorage` made the first attempt the only one.
|
|
251
|
+
*/
|
|
252
|
+
function PortalNotice({
|
|
253
|
+
answer,
|
|
254
|
+
i18n,
|
|
255
|
+
onRetry,
|
|
256
|
+
}: {
|
|
257
|
+
answer: PortalAnswer;
|
|
258
|
+
i18n: I18n;
|
|
259
|
+
onRetry: () => void;
|
|
260
|
+
}) {
|
|
261
|
+
if (answer === "refused") {
|
|
262
|
+
return (
|
|
263
|
+
<Notice
|
|
264
|
+
alert
|
|
265
|
+
icon={<ShieldOff aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
266
|
+
title={i18n.t("tools.noAccess")}
|
|
267
|
+
help={i18n.t("tools.noAccessHelp")}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
// Ran out of time on the consent screen. Nothing is broken and nobody was refused, so neither of
|
|
272
|
+
// the other sentences fits (#95).
|
|
273
|
+
if (answer === "expired") {
|
|
274
|
+
return (
|
|
275
|
+
<Notice
|
|
276
|
+
icon={<Timer aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
277
|
+
title={i18n.t("tools.connectExpired")}
|
|
278
|
+
help={i18n.t("tools.connectExpiredHelp")}
|
|
279
|
+
>
|
|
280
|
+
<Button variant="outline" onClick={onRetry}>
|
|
281
|
+
{i18n.t("tools.connectRetry")}
|
|
282
|
+
</Button>
|
|
283
|
+
</Notice>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
// A marker that is neither of the two access answers is machinery, not policy: the sign-in broke
|
|
287
|
+
// before anyone could be allowed or refused. Saying "no access" here sent the last reader hunting
|
|
288
|
+
// through Access policies while the real fault sat in a failed client registration (#93).
|
|
289
|
+
if (answer === "failed") {
|
|
290
|
+
return (
|
|
291
|
+
<Notice
|
|
292
|
+
alert
|
|
293
|
+
icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-destructive" />}
|
|
294
|
+
title={i18n.t("tools.connectFailed")}
|
|
295
|
+
help={i18n.t("tools.connectFailedHelp")}
|
|
296
|
+
>
|
|
297
|
+
<Button variant="outline" onClick={onRetry}>
|
|
298
|
+
{i18n.t("tools.connectRetry")}
|
|
299
|
+
</Button>
|
|
300
|
+
</Notice>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
// No marker at all, and an attempt already made in this browser session: it never came back. The
|
|
304
|
+
// screen says exactly that — not "no access", which is a decision nobody made here.
|
|
305
|
+
return (
|
|
306
|
+
<Notice
|
|
307
|
+
icon={<HelpCircle aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
308
|
+
title={i18n.t("tools.connectUnknown")}
|
|
309
|
+
help={i18n.t("tools.connectUnknownHelp")}
|
|
310
|
+
>
|
|
311
|
+
<Button variant="outline" onClick={onRetry}>
|
|
312
|
+
{i18n.t("tools.connectRetry")}
|
|
313
|
+
</Button>
|
|
314
|
+
</Notice>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The catalog, under the portal's servers where there are any.
|
|
320
|
+
*
|
|
321
|
+
* ⚠️ No directory, no boxes — and the screen says why instead of inventing them. A portal that does
|
|
322
|
+
* not offer `portal_list_servers`, or whose answer Intel cannot read, makes `/tools/servers` fail,
|
|
323
|
+
* and every alternative to this sentence is a guess: a cut at an underscore, or the one box called
|
|
324
|
+
* "Company portal" that held all 92 tools of the running installation (#106). The tools are all
|
|
325
|
+
* here, in the order the portal answered them; only the grouping is missing.
|
|
326
|
+
*/
|
|
327
|
+
function ToolCatalogView({
|
|
328
|
+
items,
|
|
329
|
+
servers,
|
|
330
|
+
i18n,
|
|
331
|
+
requested,
|
|
332
|
+
}: {
|
|
333
|
+
items: ToolCapability[];
|
|
334
|
+
servers: ToolServer[];
|
|
335
|
+
i18n: I18n;
|
|
336
|
+
requested: string | null;
|
|
337
|
+
}) {
|
|
338
|
+
if (servers.length === 0) {
|
|
339
|
+
return (
|
|
340
|
+
<>
|
|
341
|
+
<section className="rounded-xl border border-dashed bg-card px-5 py-4">
|
|
342
|
+
<h2 className="text-sm font-semibold">{i18n.t("tools.ungrouped")}</h2>
|
|
343
|
+
<p className="mt-1 text-sm text-muted-foreground">{i18n.t("tools.ungroupedHelp")}</p>
|
|
344
|
+
</section>
|
|
345
|
+
{/* Open, not a lid: with no server to name, a closed box would hide the whole catalog
|
|
346
|
+
behind a word that says nothing. */}
|
|
347
|
+
<section className="overflow-hidden rounded-xl border bg-card shadow-sm">
|
|
348
|
+
<ToolRows items={items} i18n={i18n} requested={requested} />
|
|
349
|
+
</section>
|
|
350
|
+
<p className="text-xs text-muted-foreground">{i18n.t("tools.liveNote")}</p>
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return (
|
|
355
|
+
<>
|
|
356
|
+
<div className="space-y-3">
|
|
357
|
+
{groupByServer(items, servers, i18n.t("tools.originUnlisted")).map((group) => (
|
|
358
|
+
<ServerGroup
|
|
359
|
+
key={group.handle ?? ""}
|
|
360
|
+
group={group}
|
|
361
|
+
note={group.handle === null ? i18n.t("tools.originUnlistedHelp") : null}
|
|
362
|
+
i18n={i18n}
|
|
363
|
+
requested={requested}
|
|
364
|
+
/>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
<p className="text-xs text-muted-foreground">{i18n.t("tools.liveNote")}</p>
|
|
368
|
+
</>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function Notice({
|
|
373
|
+
icon,
|
|
374
|
+
title,
|
|
375
|
+
help,
|
|
376
|
+
alert,
|
|
377
|
+
children,
|
|
378
|
+
}: {
|
|
379
|
+
icon: React.ReactNode;
|
|
380
|
+
title: string;
|
|
381
|
+
help: string;
|
|
382
|
+
alert?: boolean;
|
|
383
|
+
children?: React.ReactNode;
|
|
384
|
+
}) {
|
|
385
|
+
return (
|
|
386
|
+
<section
|
|
387
|
+
{...(alert ? { role: "alert" } : {})}
|
|
388
|
+
className="rounded-xl border border-dashed bg-card p-12 text-center"
|
|
389
|
+
>
|
|
390
|
+
{icon}
|
|
391
|
+
<h2 className="mt-4 font-semibold">{title}</h2>
|
|
392
|
+
<p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">{help}</p>
|
|
393
|
+
{children ? <div className="mt-5">{children}</div> : null}
|
|
394
|
+
</section>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// One server, one box, closed until asked. Closed is the point: a hundred rows spread over a
|
|
399
|
+
// handful of servers is a list nobody reads, and which server a tool speaks to is the first thing
|
|
400
|
+
// somebody wants to know about it (#99).
|
|
401
|
+
function ServerGroup({
|
|
402
|
+
group,
|
|
403
|
+
note,
|
|
404
|
+
i18n,
|
|
405
|
+
requested,
|
|
406
|
+
}: {
|
|
407
|
+
group: ToolGroup;
|
|
408
|
+
// Why this box exists at all, for the one box that is not a server. A group named by the portal
|
|
409
|
+
// needs no explanation; the remainder does, or it reads as a server called something odd.
|
|
410
|
+
note: string | null;
|
|
411
|
+
i18n: I18n;
|
|
412
|
+
requested: string | null;
|
|
413
|
+
}) {
|
|
414
|
+
// A hit from the header search names a tool, not a server, and the box it sits in has to open
|
|
415
|
+
// with it — otherwise Enter on a hit lands on a screen where nothing happened.
|
|
416
|
+
const holdsRequested = group.items.some((item) => item.name === requested);
|
|
417
|
+
const destructive = group.items.filter((item) => item.annotations.destructiveHint).length;
|
|
418
|
+
// ⚠️ One key, not a `…One` beside a `…Many` (#605): the catalog carries a wording that fits every
|
|
419
|
+
// number, so nothing here branches on the count. The branch that used to stand here is what the
|
|
420
|
+
// rule in `packages/intel/ui/CLAUDE.md` forbids — it puts the inflection into the component and into
|
|
421
|
+
// every future language.
|
|
422
|
+
const count = i18n.t("tools.toolCount", { count: group.items.length });
|
|
423
|
+
// The badges beside the name read as one run-on word when a screen reader concatenates them
|
|
424
|
+
// ("wiki1 tool"), so the control says its own name instead of being read off its contents.
|
|
425
|
+
const spoken = [`${i18n.t("tools.origin")}: ${group.label}`, count];
|
|
426
|
+
if (destructive > 0) spoken.push(i18n.t("tools.destructiveCount", { count: destructive }));
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<Collapsible defaultOpen={holdsRequested} asChild>
|
|
430
|
+
<section className="overflow-hidden rounded-xl border bg-card shadow-sm">
|
|
431
|
+
<CollapsibleTrigger
|
|
432
|
+
aria-label={spoken.join(", ")}
|
|
433
|
+
className="group flex w-full items-center gap-3 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
434
|
+
>
|
|
435
|
+
<ChevronRight
|
|
436
|
+
aria-hidden="true"
|
|
437
|
+
className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
|
|
438
|
+
/>
|
|
439
|
+
<span className="min-w-0 flex-1">
|
|
440
|
+
<span className="block truncate text-sm font-semibold">{group.label}</span>
|
|
441
|
+
</span>
|
|
442
|
+
{/* Closed, the box hides every destructive badge inside it. The count says so on the lid
|
|
443
|
+
rather than making somebody open each server to find out. */}
|
|
444
|
+
{destructive > 0 && (
|
|
445
|
+
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
|
446
|
+
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
447
|
+
{i18n.t("tools.destructiveCount", { count: destructive })}
|
|
448
|
+
</span>
|
|
449
|
+
)}
|
|
450
|
+
<span className="shrink-0 text-xs text-muted-foreground">{count}</span>
|
|
451
|
+
</CollapsibleTrigger>
|
|
452
|
+
<CollapsibleContent className="border-t">
|
|
453
|
+
{note && (
|
|
454
|
+
<p className="border-b bg-muted/50 px-5 py-3 text-xs text-muted-foreground">{note}</p>
|
|
455
|
+
)}
|
|
456
|
+
<ToolRows items={group.items} i18n={i18n} requested={requested} />
|
|
457
|
+
</CollapsibleContent>
|
|
458
|
+
</section>
|
|
459
|
+
</Collapsible>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// The rows themselves, with or without a box around them: the ungrouped screen shows the same list
|
|
464
|
+
// without a lid, and duplicating it there is how the two would drift apart.
|
|
465
|
+
function ToolRows({
|
|
466
|
+
items,
|
|
467
|
+
i18n,
|
|
468
|
+
requested,
|
|
469
|
+
}: {
|
|
470
|
+
items: ToolCapability[];
|
|
471
|
+
i18n: I18n;
|
|
472
|
+
requested: string | null;
|
|
473
|
+
}) {
|
|
474
|
+
return (
|
|
475
|
+
<ul className="divide-y">
|
|
476
|
+
{items.map((capability) => (
|
|
477
|
+
<li key={capability.name}>
|
|
478
|
+
<ToolEntry capability={capability} i18n={i18n} open={capability.name === requested} />
|
|
479
|
+
</li>
|
|
480
|
+
))}
|
|
481
|
+
</ul>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// One tool, one disclosure. The row is the only control in the list: it opens the schema and does
|
|
486
|
+
// nothing else — enabling, sharing or permitting a tool happens in the portal, never here.
|
|
487
|
+
function ToolEntry({
|
|
488
|
+
capability,
|
|
489
|
+
i18n,
|
|
490
|
+
open,
|
|
491
|
+
}: {
|
|
492
|
+
capability: ToolCapability;
|
|
493
|
+
i18n: I18n;
|
|
494
|
+
open: boolean;
|
|
495
|
+
}) {
|
|
496
|
+
return (
|
|
497
|
+
<Collapsible defaultOpen={open}>
|
|
498
|
+
<CollapsibleTrigger className="group flex w-full items-start gap-4 px-5 py-3 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
|
|
499
|
+
<ChevronRight
|
|
500
|
+
aria-hidden="true"
|
|
501
|
+
className="mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
|
|
502
|
+
/>
|
|
503
|
+
<span className="min-w-0 flex-1">
|
|
504
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
505
|
+
<span className="text-sm font-medium">{capability.title ?? capability.name}</span>
|
|
506
|
+
{capability.annotations.destructiveHint && (
|
|
507
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
|
508
|
+
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
509
|
+
{i18n.t("tools.destructiveShort")}
|
|
510
|
+
</span>
|
|
511
|
+
)}
|
|
512
|
+
</span>
|
|
513
|
+
{/* The namespaced name is what a flow references, so it stays readable — but only where
|
|
514
|
+
it is not already the heading of the row. */}
|
|
515
|
+
{capability.title && (
|
|
516
|
+
<span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
|
|
517
|
+
{capability.name}
|
|
518
|
+
</span>
|
|
519
|
+
)}
|
|
520
|
+
</span>
|
|
521
|
+
</CollapsibleTrigger>
|
|
522
|
+
{/* ⚠️ The description belongs here, not in the closed row. Held open it was the whole reason
|
|
523
|
+
the list could not be scanned: a paragraph per tool, a hundred times over. */}
|
|
524
|
+
<CollapsibleContent className="space-y-3 px-5 pb-4 pl-13">
|
|
525
|
+
{capability.description && (
|
|
526
|
+
<p className="text-xs text-muted-foreground">{capability.description}</p>
|
|
527
|
+
)}
|
|
528
|
+
<Schema label={i18n.t("tools.inputSchema")} value={capability.inputSchema} />
|
|
529
|
+
{capability.outputSchema && (
|
|
530
|
+
<Schema label={i18n.t("tools.outputSchema")} value={capability.outputSchema} />
|
|
531
|
+
)}
|
|
532
|
+
</CollapsibleContent>
|
|
533
|
+
</Collapsible>
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function Schema({ label, value }: { label: string; value: Record<string, unknown> }) {
|
|
538
|
+
return (
|
|
539
|
+
<div>
|
|
540
|
+
<p className="text-xs font-medium">{label}</p>
|
|
541
|
+
<pre className="mt-1 max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs">
|
|
542
|
+
{JSON.stringify(value, null, 2)}
|
|
543
|
+
</pre>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SessionUser } from "@contract/contract.ts";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The signed-in person, from the one query the shell already makes.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ The same query key the user footer uses, so asking here costs no second request — and the two
|
|
9
|
+
* can never disagree about who is signed in.
|
|
10
|
+
*/
|
|
11
|
+
export function useSessionUser(): SessionUser | null {
|
|
12
|
+
const { data } = useIntelRouterContext();
|
|
13
|
+
const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
|
|
14
|
+
return session.data ?? null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** What a person is CALLED, by the same rule the user footer follows: the name, or the address. */
|
|
18
|
+
export function displayNameOf(user: SessionUser): string {
|
|
19
|
+
return user.name ?? user.email;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The display name behind a user id, WITHOUT a request — or `null` where this browser cannot say
|
|
24
|
+
* from what it already holds.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ It answers for exactly one id, and that is a choice rather than a limit: the signed-in
|
|
27
|
+
* person's, out of the session the shell already fetched. Intel CAN name a foreign id — the bundled
|
|
28
|
+
* resolution over `directory_resolve` does it (`#700`) — but not from here. A hook that resolved a
|
|
29
|
+
* single id would be called once per row and once per circle, and one screen would open one request
|
|
30
|
+
* per person instead of one for all of them.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ So a caller that needs foreign names asks for them where the ids are already in one place, and
|
|
33
|
+
* layers the two: this hook first, the resolved map second. `useAssigneeLabel` is the worked
|
|
34
|
+
* example.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ Never fall back to the id. An id under a name is not an answer to "who made this", it is the
|
|
37
|
+
* question again in smaller type — and it would leak an identifier onto a screen that has no reason
|
|
38
|
+
* to carry one. Callers show `node.someUser` instead (#258).
|
|
39
|
+
*/
|
|
40
|
+
export function useUserName(userId: string | null): string | null {
|
|
41
|
+
const me = useSessionUser();
|
|
42
|
+
if (userId === null || me === null || me.id !== userId) return null;
|
|
43
|
+
return displayNameOf(me);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The one or two letters that stand for a name in a circle.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ Lives here rather than beside the circle that first needed it: two surfaces now draw the same
|
|
50
|
+
* circle — the access summary and a task's assignee — and two copies of this would drift into two
|
|
51
|
+
* different abbreviations of the same person.
|
|
52
|
+
*/
|
|
53
|
+
export function initials(value: string): string {
|
|
54
|
+
const parts = value
|
|
55
|
+
.replace(/@.*$/, "")
|
|
56
|
+
.split(/[\s._+-]+/)
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
if (parts.length === 0) return "?";
|
|
59
|
+
if (parts.length === 1) return (parts[0]?.slice(0, 2) ?? "?").toUpperCase();
|
|
60
|
+
return `${parts[0]?.[0] ?? ""}${parts.at(-1)?.[0] ?? ""}`.toUpperCase();
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A piece of the document the editor cannot show, kept exactly as it was written (#826).
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ **This is the frontmatter's way, generalised.** #657 gave the head its own path because
|
|
8
|
+
* `blocksToMarkdownLossy` had destroyed it: what the editor does not understand it rewrites into
|
|
9
|
+
* what it does, and `payload.markdown` is what the search reads. The head was the only construct
|
|
10
|
+
* with that protection; everything else the editor did not know still lost its shape on the first
|
|
11
|
+
* open-and-save, and nothing anywhere went red.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **`raw` is the exact source text, never something parsed.** Whatever went in comes back out,
|
|
14
|
+
* byte for byte. A block that carried a parsed structure would turn every gap in the parser into
|
|
15
|
+
* data loss at the next save, which is the trap the head already avoids.
|
|
16
|
+
*/
|
|
17
|
+
export const VerbatimBlockType = "verbatim";
|
|
18
|
+
|
|
19
|
+
const VERBATIM_PROPS = { raw: { default: "" } } as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The one word the block says. It travels in a context for the same reason the head's two do: a
|
|
23
|
+
* block is rendered by the editor, not by a view that could reach the catalogue.
|
|
24
|
+
*/
|
|
25
|
+
const VerbatimContext = createContext<string>("Kept as written");
|
|
26
|
+
|
|
27
|
+
export const VerbatimProvider = VerbatimContext.Provider;
|
|
28
|
+
|
|
29
|
+
export function VerbatimBlock({ raw }: { raw: string }) {
|
|
30
|
+
const label = useContext(VerbatimContext);
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
// The editor owns the text around this; the block itself is not typed into. What is in here
|
|
34
|
+
// is the author's own text, and an editor that cannot draw it must not let anybody change it
|
|
35
|
+
// by accident either.
|
|
36
|
+
contentEditable={false}
|
|
37
|
+
data-verbatim=""
|
|
38
|
+
className="w-full overflow-x-auto rounded-md bg-muted/40 px-3 py-2"
|
|
39
|
+
>
|
|
40
|
+
{/* ⚠️ Named rather than silent, which is the whole difference to the state before: a piece
|
|
41
|
+
that vanishes without a word is what #826 is about. */}
|
|
42
|
+
<p className="mb-1 text-xs text-muted-foreground">{label}</p>
|
|
43
|
+
<pre className="whitespace-pre font-mono text-xs">
|
|
44
|
+
<code>{raw}</code>
|
|
45
|
+
</pre>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const verbatimSpec = createReactBlockSpec(
|
|
51
|
+
{
|
|
52
|
+
type: VerbatimBlockType,
|
|
53
|
+
propSchema: VERBATIM_PROPS,
|
|
54
|
+
content: "none",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
render: (props) => <VerbatimBlock raw={String(props.block.props.raw)} />,
|
|
58
|
+
// ⚠️ The net under `blocksToMarkdown`, for every path that does not go through it. Without a
|
|
59
|
+
// serialiser BlockNote falls back to the RENDERED DOM, and the label would end up in the
|
|
60
|
+
// document in place of the text. `<pre><code>` rather than a bare `<pre>`: measured in #657, a
|
|
61
|
+
// bare one survives the HTML export and is then dropped by the HTML-to-markdown step.
|
|
62
|
+
toExternalHTML: (props) => (
|
|
63
|
+
<pre>
|
|
64
|
+
<code>{String(props.block.props.raw)}</code>
|
|
65
|
+
</pre>
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
);
|