@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,87 @@
|
|
|
1
|
+
import { Outlet } from "@tanstack/react-router";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { AppSidebar } from "@/app/app-sidebar/app-sidebar.tsx";
|
|
4
|
+
import { HeaderSearch } from "@/app/header-search/header-search.tsx";
|
|
5
|
+
import { createSidebarPreferences } from "@/app/sidebar-preferences/sidebar-preferences.ts";
|
|
6
|
+
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
|
7
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
8
|
+
|
|
9
|
+
// ⚠️ The breadcrumb is gone (#337). It was two levels deep and the first of them was constant —
|
|
10
|
+
// `Intel > Knowledge`, `Intel > Flows`, `Intel > Tools`. The product name stands in the sidebar
|
|
11
|
+
// anyway, the area is marked there as the active row, and what was left was a path that never grew
|
|
12
|
+
// past two links and carried no navigation that did not already exist. The `Separator` beside the
|
|
13
|
+
// sidebar trigger went with it: it separated the trigger from the breadcrumb, and there is nothing
|
|
14
|
+
// left on that side to separate it from.
|
|
15
|
+
export function App() {
|
|
16
|
+
const i18n = useI18n();
|
|
17
|
+
|
|
18
|
+
// Read once: the stored preference is the starting point, after which the shell's own state is
|
|
19
|
+
// the truth. Re-reading on every render would fight the drag.
|
|
20
|
+
const preferences = useRef(createSidebarPreferences()).current;
|
|
21
|
+
const stored = useRef(preferences.read()).current;
|
|
22
|
+
const [open, setOpen] = useState(stored.open);
|
|
23
|
+
const [width, setWidth] = useState(stored.width);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
// ⚠️ `h-svh` is what keeps the box inside the screen. The registry's wrapper only carries
|
|
27
|
+
// `min-h-svh`, so its height follows its content — and the inset, an `inset` variant with a
|
|
28
|
+
// margin all round, then stands 16px taller than the viewport at best and grows with a long
|
|
29
|
+
// document at worst. Both times the lower border and its two rounded corners end up under the
|
|
30
|
+
// fold and the whole page scrolls. With a definite height the inset stretches to
|
|
31
|
+
// `100svh - 16px`, and the overflow below belongs to the container around the outlet, which
|
|
32
|
+
// already carries `min-h-0 overflow-auto` for exactly that. Set here rather than in
|
|
33
|
+
// `components/ui/sidebar.tsx`: that is registry code and an edit in it is silently gone at the
|
|
34
|
+
// next sync.
|
|
35
|
+
<SidebarProvider
|
|
36
|
+
className="h-svh"
|
|
37
|
+
open={open}
|
|
38
|
+
onOpenChange={(next) => {
|
|
39
|
+
setOpen(next);
|
|
40
|
+
preferences.write({ open: next, width });
|
|
41
|
+
}}
|
|
42
|
+
style={{ "--sidebar-width": `${width}px` } as React.CSSProperties}
|
|
43
|
+
>
|
|
44
|
+
<AppSidebar
|
|
45
|
+
width={width}
|
|
46
|
+
onWidthChange={(next) => {
|
|
47
|
+
setWidth(next);
|
|
48
|
+
preferences.write({ open, width: next });
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
{/* ⚠️ `min-w-0` is not cosmetic: the inset is a flex item beside the sidebar and carries
|
|
52
|
+
`min-width: auto`, so it never shrinks below its content's min-content width. One wide
|
|
53
|
+
child (a flow canvas, a table of raw JSON) would widen the page instead of scrolling
|
|
54
|
+
inside its own container. Learned in gate, GATE-76 D. */}
|
|
55
|
+
{/* `overflow-hidden` is the second half of the rounding: the box is rounded but nothing
|
|
56
|
+
clipped its children, so an opaque panel inside it — the flow editor's inspector carries
|
|
57
|
+
`bg-card` — painted its own square corner over the rounded one. Invisible in the light
|
|
58
|
+
theme, where card and background are the same white, and plain to see in the dark one.
|
|
59
|
+
Scrolling is unaffected: the container around the outlet below owns it. */}
|
|
60
|
+
<SidebarInset className="min-w-0 overflow-hidden md:border">
|
|
61
|
+
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
|
|
62
|
+
<SidebarTrigger className="-ml-1" aria-label={i18n.t("shell.toggleSidebar")} />
|
|
63
|
+
{/* The bar for area-owned actions. A screen renders into the inner box through
|
|
64
|
+
`ActionSlot`; search is the shell's own, because it has to open from every screen
|
|
65
|
+
alike and belongs to none of them. It stays last, against the right edge, and a
|
|
66
|
+
screen's actions line up to its left: the search is the one thing in this bar that is
|
|
67
|
+
on every screen, so it is the one whose place must not depend on what a screen happens
|
|
68
|
+
to bring. Pinned the other way round it was the constant that moved (#56).
|
|
69
|
+
|
|
70
|
+
⚠️ Two boxes rather than one list, and that is the whole of the mechanism: a portal
|
|
71
|
+
only ever appends to its container, so with the search a sibling of the portalled
|
|
72
|
+
actions a screen that mounts them later — Flows does, once a flow is selected — would
|
|
73
|
+
land to its right. Giving the screens a container of their own takes mount order out
|
|
74
|
+
of the question entirely. `data-slot="header-actions"` therefore names the inner box:
|
|
75
|
+
it is the name every `ActionSlot` looks up. */}
|
|
76
|
+
<div data-slot="header-bar" className="ml-auto flex items-center gap-2">
|
|
77
|
+
<div data-slot="header-actions" className="flex items-center gap-2" />
|
|
78
|
+
<HeaderSearch />
|
|
79
|
+
</div>
|
|
80
|
+
</header>
|
|
81
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-auto">
|
|
82
|
+
<Outlet />
|
|
83
|
+
</div>
|
|
84
|
+
</SidebarInset>
|
|
85
|
+
</SidebarProvider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { Flow } from "@contract/flow.ts";
|
|
2
|
+
import type { ToolCapability } from "@contract/tool.ts";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
5
|
+
import { FileText, Search, Workflow, Wrench } from "lucide-react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Command,
|
|
9
|
+
CommandEmpty,
|
|
10
|
+
CommandGroup,
|
|
11
|
+
CommandInput,
|
|
12
|
+
CommandItem,
|
|
13
|
+
CommandList,
|
|
14
|
+
} from "@/components/ui/command";
|
|
15
|
+
import {
|
|
16
|
+
Dialog,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogDescription,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
} from "@/components/ui/dialog";
|
|
22
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
23
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
24
|
+
|
|
25
|
+
const Kinds = ["node", "flow", "tool"] as const;
|
|
26
|
+
type Kind = (typeof Kinds)[number];
|
|
27
|
+
|
|
28
|
+
// Enough to answer "where was that again" without turning the dialog into a result page.
|
|
29
|
+
const ResultLimit = 8;
|
|
30
|
+
|
|
31
|
+
const areaFor = { node: "/nodes", flow: "/flows", tool: "/tools" } as const;
|
|
32
|
+
|
|
33
|
+
function isMac(): boolean {
|
|
34
|
+
return typeof navigator !== "undefined" && /mac/i.test(navigator.userAgent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ⚠️ The deliberate boundary of this ticket, named rather than hidden: Intel has a search endpoint
|
|
38
|
+
// for nodes only, so node hits come from `searchNodes()` and the server decides both
|
|
39
|
+
// what matches and what may be seen. Flows and Tools have no such endpoint — they are the same
|
|
40
|
+
// authorized lists the screens render, narrowed here in the client.
|
|
41
|
+
//
|
|
42
|
+
// That is safe in the one direction that matters: `listFlows()` returns what the caller may see
|
|
43
|
+
// (the flow repository's `listVisible`), and `listTools()` is one live `tools/list` with the user's
|
|
44
|
+
// own portal token. The client narrows a permitted set; it can never widen one, and no permission
|
|
45
|
+
// decision is taken here.
|
|
46
|
+
//
|
|
47
|
+
// When either list outgrows what a browser should hold, that is a new ticket for a search endpoint
|
|
48
|
+
// — not a silent regression of this one.
|
|
49
|
+
function matches(query: string, ...fields: (string | null)[]): boolean {
|
|
50
|
+
const needle = query.toLocaleLowerCase();
|
|
51
|
+
return fields.some((field) => field?.toLocaleLowerCase().includes(needle));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function HeaderSearch() {
|
|
55
|
+
const { data } = useIntelRouterContext();
|
|
56
|
+
const i18n = useI18n();
|
|
57
|
+
const navigate = useNavigate();
|
|
58
|
+
const [open, setOpen] = useState(false);
|
|
59
|
+
const [text, setText] = useState("");
|
|
60
|
+
const [kind, setKind] = useState<Kind | null>(null);
|
|
61
|
+
const query = text.trim();
|
|
62
|
+
const restoreTo = useRef<HTMLElement | null>(null);
|
|
63
|
+
|
|
64
|
+
function change(next: boolean) {
|
|
65
|
+
if (next) restoreTo.current = document.activeElement as HTMLElement | null;
|
|
66
|
+
setOpen(next);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The same combination opens and closes the dialog. Radix does not swallow the key while it is
|
|
70
|
+
// open, so one toggling listener on the window covers both directions from every screen.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
73
|
+
if (event.key.toLowerCase() !== "k" || !(event.metaKey || event.ctrlKey) || event.altKey) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
if (!open) restoreTo.current = document.activeElement as HTMLElement | null;
|
|
78
|
+
setOpen(!open);
|
|
79
|
+
}
|
|
80
|
+
window.addEventListener("keydown", onKeyDown);
|
|
81
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
82
|
+
}, [open]);
|
|
83
|
+
|
|
84
|
+
const shows = (candidate: Kind) => kind === null || kind === candidate;
|
|
85
|
+
// A filter that excludes a kind does not ask for it either: no request, and therefore no group
|
|
86
|
+
// that could show up empty.
|
|
87
|
+
const nodes = useQuery({
|
|
88
|
+
queryKey: ["node-search", query],
|
|
89
|
+
queryFn: () => data.searchNodes({ query, limit: ResultLimit }),
|
|
90
|
+
enabled: open && shows("node") && query.length > 0,
|
|
91
|
+
});
|
|
92
|
+
// ⚠️ Deliberately the whole list, not one folder of the tree: a search that only saw the level
|
|
93
|
+
// someone happened to have open would miss the flow they are looking for. The sidebar tree asks
|
|
94
|
+
// per folder and keeps its own keys; only the Tools key is shared with the screen that fills it.
|
|
95
|
+
const flows = useQuery({
|
|
96
|
+
queryKey: ["flows"],
|
|
97
|
+
queryFn: () => data.listFlows(),
|
|
98
|
+
enabled: open && shows("flow") && query.length > 0,
|
|
99
|
+
});
|
|
100
|
+
const tools = useQuery({
|
|
101
|
+
queryKey: ["tools"],
|
|
102
|
+
queryFn: () => data.listTools(),
|
|
103
|
+
enabled: open && shows("tool") && query.length > 0,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const active = [
|
|
107
|
+
...(shows("node") ? [nodes] : []),
|
|
108
|
+
...(shows("flow") ? [flows] : []),
|
|
109
|
+
...(shows("tool") ? [tools] : []),
|
|
110
|
+
];
|
|
111
|
+
const searching = query.length > 0 && active.some((source) => source.isPending);
|
|
112
|
+
const failed = query.length > 0 && active.some((source) => source.isError);
|
|
113
|
+
|
|
114
|
+
const nodeHits = shows("node") ? (nodes.data?.items ?? []) : [];
|
|
115
|
+
const flowHits: Flow[] = shows("flow")
|
|
116
|
+
? (flows.data?.items ?? [])
|
|
117
|
+
.filter((flow) => matches(query, flow.title, flow.description))
|
|
118
|
+
.slice(0, ResultLimit)
|
|
119
|
+
: [];
|
|
120
|
+
const toolHits: ToolCapability[] = shows("tool")
|
|
121
|
+
? (tools.data?.items ?? [])
|
|
122
|
+
.filter((tool) => matches(query, tool.name, tool.title, tool.description))
|
|
123
|
+
.slice(0, ResultLimit)
|
|
124
|
+
: [];
|
|
125
|
+
const total = nodeHits.length + flowHits.length + toolHits.length;
|
|
126
|
+
|
|
127
|
+
function reach(hit: Kind, select: string) {
|
|
128
|
+
setOpen(false);
|
|
129
|
+
void navigate({ to: areaFor[hit], search: { select } });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={() => change(true)}
|
|
137
|
+
aria-label={i18n.t("search.open")}
|
|
138
|
+
aria-keyshortcuts="Meta+K Control+K"
|
|
139
|
+
className="flex h-8 items-center gap-2 rounded-md border bg-background px-2.5 text-sm text-muted-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring sm:w-56"
|
|
140
|
+
>
|
|
141
|
+
<Search aria-hidden="true" className="size-4 shrink-0" />
|
|
142
|
+
<span className="truncate">{i18n.t("search.open")}</span>
|
|
143
|
+
{/* The hint repeats what `aria-keyshortcuts` already announces, so the accessible name
|
|
144
|
+
stays the label above rather than "Search ⌘K". */}
|
|
145
|
+
<kbd className="ml-auto hidden rounded border bg-muted px-1.5 font-sans text-[10px] sm:inline">
|
|
146
|
+
{isMac() ? "⌘K" : "Ctrl K"}
|
|
147
|
+
</kbd>
|
|
148
|
+
</button>
|
|
149
|
+
|
|
150
|
+
{/* ⚠️ Composed from `Dialog` and `Command` rather than from the registry's `CommandDialog`
|
|
151
|
+
for two reasons that both matter here: `CommandDialog` forwards its extra props to the
|
|
152
|
+
dialog root, so `shouldFilter` would never reach cmdk, and it renders its title outside
|
|
153
|
+
`DialogContent`, where it does not name the dialog for a screen reader. */}
|
|
154
|
+
<Dialog open={open} onOpenChange={change}>
|
|
155
|
+
<DialogContent
|
|
156
|
+
className="overflow-hidden p-0"
|
|
157
|
+
// ⚠️ Radix hands focus back to the dialog's trigger. This dialog is usually opened by a
|
|
158
|
+
// shortcut and has no trigger at all, so without this the caret would land on the body
|
|
159
|
+
// and a keyboard user would start over at the top of the page.
|
|
160
|
+
onCloseAutoFocus={(event) => {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
restoreTo.current?.focus();
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<DialogHeader className="sr-only">
|
|
166
|
+
<DialogTitle>{i18n.t("search.title")}</DialogTitle>
|
|
167
|
+
<DialogDescription>{i18n.t("search.description")}</DialogDescription>
|
|
168
|
+
</DialogHeader>
|
|
169
|
+
{/* Every source has already decided what matches — the server for nodes, the client
|
|
170
|
+
filter above for the two lists. Letting cmdk filter again would hide rows the server
|
|
171
|
+
returned, because a row's value is its identity rather than its text. */}
|
|
172
|
+
<Command shouldFilter={false}>
|
|
173
|
+
<CommandInput
|
|
174
|
+
value={text}
|
|
175
|
+
onValueChange={setText}
|
|
176
|
+
placeholder={i18n.t("search.placeholder")}
|
|
177
|
+
/>
|
|
178
|
+
<fieldset className="flex flex-wrap gap-1 border-b px-3 py-2">
|
|
179
|
+
<legend className="sr-only">{i18n.t("search.filter")}</legend>
|
|
180
|
+
{[null, ...Kinds].map((candidate) => (
|
|
181
|
+
<button
|
|
182
|
+
key={candidate ?? "all"}
|
|
183
|
+
type="button"
|
|
184
|
+
aria-pressed={kind === candidate}
|
|
185
|
+
onClick={() => setKind(candidate)}
|
|
186
|
+
// ⚠️ cmdk turns Enter into "open the highlighted result" on the command root,
|
|
187
|
+
// without looking at where the key came from. Unstopped, Enter on a filter would
|
|
188
|
+
// leave the dialog for a hit instead of narrowing the list.
|
|
189
|
+
onKeyDown={(event) => {
|
|
190
|
+
if (event.key === "Enter" || event.key === " ") event.stopPropagation();
|
|
191
|
+
}}
|
|
192
|
+
className="rounded-full border px-2.5 py-1 text-xs outline-none aria-pressed:bg-primary aria-pressed:text-primary-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
193
|
+
>
|
|
194
|
+
{i18n.t(candidate === null ? "search.all" : `search.kind.${candidate}`)}
|
|
195
|
+
</button>
|
|
196
|
+
))}
|
|
197
|
+
</fieldset>
|
|
198
|
+
<CommandList>
|
|
199
|
+
{query.length === 0 ? (
|
|
200
|
+
<p className="py-6 text-center text-sm text-muted-foreground">
|
|
201
|
+
{i18n.t("search.hint")}
|
|
202
|
+
</p>
|
|
203
|
+
) : null}
|
|
204
|
+
{/* Three states that never read as one another: still asking, could not ask, and asked
|
|
205
|
+
with nothing to show. A failing source is an alert even when another one answered. */}
|
|
206
|
+
{failed ? (
|
|
207
|
+
<div role="alert" className="space-y-3 px-3 py-6 text-center text-sm">
|
|
208
|
+
<p className="text-destructive">{i18n.t("search.failed")}</p>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
onClick={() => {
|
|
212
|
+
for (const source of active) if (source.isError) void source.refetch();
|
|
213
|
+
}}
|
|
214
|
+
className="rounded-md border px-2.5 py-1 text-xs outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
215
|
+
>
|
|
216
|
+
{i18n.t("common.retry")}
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
) : null}
|
|
220
|
+
{searching ? (
|
|
221
|
+
<p role="status" className="py-6 text-center text-sm text-muted-foreground">
|
|
222
|
+
{i18n.t("search.searching")}
|
|
223
|
+
</p>
|
|
224
|
+
) : null}
|
|
225
|
+
{!searching && !failed && query.length > 0 && total === 0 ? (
|
|
226
|
+
<CommandEmpty>{i18n.t("search.empty", { query })}</CommandEmpty>
|
|
227
|
+
) : null}
|
|
228
|
+
|
|
229
|
+
{nodeHits.length > 0 ? (
|
|
230
|
+
<CommandGroup heading={i18n.t("search.kind.node")}>
|
|
231
|
+
{nodeHits.map((citation) => (
|
|
232
|
+
<CommandItem
|
|
233
|
+
key={`${citation.nodeId}:${citation.versionId}`}
|
|
234
|
+
value={`node:${citation.nodeId}:${citation.versionId}`}
|
|
235
|
+
onSelect={() => reach("node", citation.nodeId)}
|
|
236
|
+
>
|
|
237
|
+
<FileText aria-hidden="true" />
|
|
238
|
+
<span className="min-w-0 flex-1">
|
|
239
|
+
<span className="block truncate">{citation.title}</span>
|
|
240
|
+
<span className="block truncate text-xs text-muted-foreground">
|
|
241
|
+
{citation.passage}
|
|
242
|
+
</span>
|
|
243
|
+
</span>
|
|
244
|
+
</CommandItem>
|
|
245
|
+
))}
|
|
246
|
+
</CommandGroup>
|
|
247
|
+
) : null}
|
|
248
|
+
{flowHits.length > 0 ? (
|
|
249
|
+
<CommandGroup heading={i18n.t("search.kind.flow")}>
|
|
250
|
+
{flowHits.map((flow) => (
|
|
251
|
+
<CommandItem
|
|
252
|
+
key={flow.id}
|
|
253
|
+
value={`flow:${flow.id}`}
|
|
254
|
+
onSelect={() => reach("flow", flow.id)}
|
|
255
|
+
>
|
|
256
|
+
<Workflow aria-hidden="true" />
|
|
257
|
+
<span className="min-w-0 flex-1">
|
|
258
|
+
<span className="block truncate">{flow.title}</span>
|
|
259
|
+
{flow.description ? (
|
|
260
|
+
<span className="block truncate text-xs text-muted-foreground">
|
|
261
|
+
{flow.description}
|
|
262
|
+
</span>
|
|
263
|
+
) : null}
|
|
264
|
+
</span>
|
|
265
|
+
</CommandItem>
|
|
266
|
+
))}
|
|
267
|
+
</CommandGroup>
|
|
268
|
+
) : null}
|
|
269
|
+
{toolHits.length > 0 ? (
|
|
270
|
+
<CommandGroup heading={i18n.t("search.kind.tool")}>
|
|
271
|
+
{toolHits.map((tool) => (
|
|
272
|
+
<CommandItem
|
|
273
|
+
key={tool.name}
|
|
274
|
+
value={`tool:${tool.name}`}
|
|
275
|
+
onSelect={() => reach("tool", tool.name)}
|
|
276
|
+
>
|
|
277
|
+
<Wrench aria-hidden="true" />
|
|
278
|
+
<span className="min-w-0 flex-1">
|
|
279
|
+
<span className="block truncate">{tool.title ?? tool.name}</span>
|
|
280
|
+
<span className="block truncate font-mono text-xs text-muted-foreground">
|
|
281
|
+
{tool.name}
|
|
282
|
+
</span>
|
|
283
|
+
</span>
|
|
284
|
+
</CommandItem>
|
|
285
|
+
))}
|
|
286
|
+
</CommandGroup>
|
|
287
|
+
) : null}
|
|
288
|
+
</CommandList>
|
|
289
|
+
</Command>
|
|
290
|
+
</DialogContent>
|
|
291
|
+
</Dialog>
|
|
292
|
+
</>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
12
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rebuilding the search index, asked for before it happens (#416).
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Two clicks, not one, and the reason is not that it is dangerous — nothing is lost, the index is
|
|
18
|
+
* derived from D1 and R2 and can always be built again. It is that it is EXPENSIVE and invisible:
|
|
19
|
+
* every node is read and embedded again, which costs money and minutes, and the screen looks exactly
|
|
20
|
+
* the same afterwards. A one-click button next to "Preferences" invites a second press when the
|
|
21
|
+
* first one seems to have done nothing.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ It reports what was QUEUED, never "done". The answer comes back the moment the work is handed
|
|
24
|
+
* to the queue, and the index catches up behind it — writing "finished" here would be a sentence
|
|
25
|
+
* about something this screen cannot see. The number is the honest part: it says how much was
|
|
26
|
+
* accepted, so a repeat press can be compared against it.
|
|
27
|
+
*
|
|
28
|
+
* The dialog is opened from outside, like the settings one: its trigger is a `DropdownMenuItem`, and
|
|
29
|
+
* Radix unmounts the menu content in the same frame the item is chosen.
|
|
30
|
+
*/
|
|
31
|
+
interface ReindexDialogProps {
|
|
32
|
+
open: boolean;
|
|
33
|
+
onOpenChange(open: boolean): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ReindexDialog({ open, onOpenChange }: ReindexDialogProps) {
|
|
37
|
+
const i18n = useI18n();
|
|
38
|
+
const { data } = useIntelRouterContext();
|
|
39
|
+
const reindex = useMutation({ mutationFn: () => data.reindexNodes() });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Dialog
|
|
43
|
+
open={open}
|
|
44
|
+
onOpenChange={(next) => {
|
|
45
|
+
// Closing forgets the last answer, so re-opening never shows a count from an earlier run as
|
|
46
|
+
// if it were this one's.
|
|
47
|
+
if (!next) reindex.reset();
|
|
48
|
+
onOpenChange(next);
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<DialogContent className="sm:max-w-md">
|
|
52
|
+
<DialogHeader>
|
|
53
|
+
<DialogTitle>{i18n.t("reindex.title")}</DialogTitle>
|
|
54
|
+
<DialogDescription>{i18n.t("reindex.description")}</DialogDescription>
|
|
55
|
+
</DialogHeader>
|
|
56
|
+
{reindex.isSuccess ? (
|
|
57
|
+
<p role="status" className="text-sm">
|
|
58
|
+
{i18n.t("reindex.queued", { count: reindex.data.queued })}
|
|
59
|
+
</p>
|
|
60
|
+
) : null}
|
|
61
|
+
{reindex.isError ? (
|
|
62
|
+
<p role="alert" className="text-sm text-destructive">
|
|
63
|
+
{i18n.t("reindex.failed")}
|
|
64
|
+
</p>
|
|
65
|
+
) : null}
|
|
66
|
+
<DialogFooter>
|
|
67
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
68
|
+
{i18n.t(reindex.isSuccess ? "reindex.close" : "reindex.cancel")}
|
|
69
|
+
</Button>
|
|
70
|
+
{reindex.isSuccess ? null : (
|
|
71
|
+
<Button disabled={reindex.isPending} onClick={() => reindex.mutate()}>
|
|
72
|
+
{i18n.t(reindex.isPending ? "reindex.running" : "reindex.confirm")}
|
|
73
|
+
</Button>
|
|
74
|
+
)}
|
|
75
|
+
</DialogFooter>
|
|
76
|
+
</DialogContent>
|
|
77
|
+
</Dialog>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dialog,
|
|
3
|
+
DialogContent,
|
|
4
|
+
DialogDescription,
|
|
5
|
+
DialogHeader,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
} from "@/components/ui/dialog";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@/components/ui/select";
|
|
15
|
+
import { useI18n, useLanguage } from "@/i18n/i18n-context.tsx";
|
|
16
|
+
import { languageName } from "@/i18n/i18n-languages/i18n-languages.ts";
|
|
17
|
+
import { type ThemeChoice, ThemeChoices } from "@/theme/theme.ts";
|
|
18
|
+
import { useTheme } from "@/theme/theme-context.tsx";
|
|
19
|
+
import { DeviceZone, deviceZone, ZoneOptions } from "@/time/time.ts";
|
|
20
|
+
import { useTimeZone } from "@/time/time-context.tsx";
|
|
21
|
+
|
|
22
|
+
// The settings that belong to the person rather than to the installation: language and appearance.
|
|
23
|
+
// Both are client state — they live in localStorage, not in the Intel HTTP surface — so they do not
|
|
24
|
+
// belong on a screen with a save button. A choice applies at once; a dialog that previews and only
|
|
25
|
+
// commits on "Save" would carry two states too many for two dropdowns.
|
|
26
|
+
//
|
|
27
|
+
// ⚠️ The third one, the reader's timezone, is BACK — and the sentence that sent it away is the
|
|
28
|
+
// reason it could come back (#467). It left with the Agents (#388) because it was a suggestion for
|
|
29
|
+
// a schedule and the lens its calendar was read through (#228); with both gone nothing read the
|
|
30
|
+
// value, "and a preference that changes nothing on screen is worse than none — it invites the
|
|
31
|
+
// reader to set it and then contradicts them. Reading times in a chosen zone is a feature, and it
|
|
32
|
+
// would come back as one."
|
|
33
|
+
//
|
|
34
|
+
// It came back as one, and in that order: FIRST every time on screen reads `useDateTime()`, THEN
|
|
35
|
+
// this dropdown exists. Whoever adds a surface that shows a time reads that hook — a second
|
|
36
|
+
// `toLocaleString` would be the old state again, only harder to find.
|
|
37
|
+
//
|
|
38
|
+
// The dialog is opened from outside (the user footer holds the state), because its trigger is a
|
|
39
|
+
// DropdownMenuItem: Radix unmounts the menu content when an item is chosen and would take a dialog
|
|
40
|
+
// nested inside it along in the same frame.
|
|
41
|
+
interface SettingsDialogProps {
|
|
42
|
+
open: boolean;
|
|
43
|
+
onOpenChange(open: boolean): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|
47
|
+
const i18n = useI18n();
|
|
48
|
+
const language = useLanguage();
|
|
49
|
+
const theme = useTheme();
|
|
50
|
+
const zone = useTimeZone();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
54
|
+
<DialogContent className="sm:max-w-md">
|
|
55
|
+
<DialogHeader>
|
|
56
|
+
<DialogTitle>{i18n.t("settings.title")}</DialogTitle>
|
|
57
|
+
<DialogDescription>{i18n.t("settings.description")}</DialogDescription>
|
|
58
|
+
</DialogHeader>
|
|
59
|
+
<div className="grid gap-6">
|
|
60
|
+
{/* Without a provider there is nothing to choose — then the block is absent rather than a
|
|
61
|
+
dead dropdown. The number of languages is deliberately NOT checked: there are always
|
|
62
|
+
at least the three built-in ones, so a condition on it would be dead code. */}
|
|
63
|
+
{language ? (
|
|
64
|
+
<div className="grid gap-2">
|
|
65
|
+
<label className="text-sm font-medium" htmlFor="settings-language">
|
|
66
|
+
{i18n.t("settings.language")}
|
|
67
|
+
</label>
|
|
68
|
+
<Select value={language.current} onValueChange={language.select}>
|
|
69
|
+
<SelectTrigger id="settings-language" className="w-full">
|
|
70
|
+
<SelectValue />
|
|
71
|
+
</SelectTrigger>
|
|
72
|
+
<SelectContent>
|
|
73
|
+
{language.codes.map((code) => (
|
|
74
|
+
<SelectItem key={code} value={code}>
|
|
75
|
+
{languageName(code)}
|
|
76
|
+
</SelectItem>
|
|
77
|
+
))}
|
|
78
|
+
</SelectContent>
|
|
79
|
+
</Select>
|
|
80
|
+
</div>
|
|
81
|
+
) : null}
|
|
82
|
+
{theme ? (
|
|
83
|
+
<div className="grid gap-2">
|
|
84
|
+
<label className="text-sm font-medium" htmlFor="settings-appearance">
|
|
85
|
+
{i18n.t("settings.appearance")}
|
|
86
|
+
</label>
|
|
87
|
+
{/* Radix hands out a raw string; the values come from the same list that fills the
|
|
88
|
+
menu, so the assertion here is the whole check. */}
|
|
89
|
+
<Select
|
|
90
|
+
value={theme.current}
|
|
91
|
+
onValueChange={(value) => theme.select(value as ThemeChoice)}
|
|
92
|
+
>
|
|
93
|
+
<SelectTrigger id="settings-appearance" className="w-full">
|
|
94
|
+
<SelectValue />
|
|
95
|
+
</SelectTrigger>
|
|
96
|
+
<SelectContent>
|
|
97
|
+
{ThemeChoices.map((choice) => (
|
|
98
|
+
<SelectItem key={choice} value={choice}>
|
|
99
|
+
{i18n.t(`settings.appearance.${choice}`)}
|
|
100
|
+
</SelectItem>
|
|
101
|
+
))}
|
|
102
|
+
</SelectContent>
|
|
103
|
+
</Select>
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
106
|
+
{zone ? (
|
|
107
|
+
<div className="grid gap-2">
|
|
108
|
+
<label className="text-sm font-medium" htmlFor="settings-timezone">
|
|
109
|
+
{i18n.t("settings.timezone")}
|
|
110
|
+
</label>
|
|
111
|
+
<Select value={zone.current} onValueChange={zone.select}>
|
|
112
|
+
<SelectTrigger id="settings-timezone" className="w-full">
|
|
113
|
+
<SelectValue />
|
|
114
|
+
</SelectTrigger>
|
|
115
|
+
<SelectContent>
|
|
116
|
+
{/* The default is the device's zone and is NAMED as such — not an empty field
|
|
117
|
+
that would look as if nothing were set. */}
|
|
118
|
+
<SelectItem value={DeviceZone}>
|
|
119
|
+
{i18n.t("settings.timezone.device", { zone: deviceZone() })}
|
|
120
|
+
</SelectItem>
|
|
121
|
+
{ZoneOptions.map((option) => (
|
|
122
|
+
<SelectItem key={option} value={option}>
|
|
123
|
+
{option}
|
|
124
|
+
</SelectItem>
|
|
125
|
+
))}
|
|
126
|
+
</SelectContent>
|
|
127
|
+
</Select>
|
|
128
|
+
<p className="text-xs text-muted-foreground">{i18n.t("settings.timezone.hint")}</p>
|
|
129
|
+
</div>
|
|
130
|
+
) : null}
|
|
131
|
+
</div>
|
|
132
|
+
</DialogContent>
|
|
133
|
+
</Dialog>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SidebarPreferenceDeps,
|
|
3
|
+
SidebarPreferenceStore,
|
|
4
|
+
SidebarPreferences,
|
|
5
|
+
} from "./sidebar-preferences.types.ts";
|
|
6
|
+
|
|
7
|
+
// Below the minimum the tree is unreadable and the sidebar cannot be dragged back open by eye;
|
|
8
|
+
// above the maximum the content box stops being the larger half. Both are the reason the drag
|
|
9
|
+
// exists at all, so neither is configurable.
|
|
10
|
+
export const MinSidebarWidth = 180;
|
|
11
|
+
export const MaxSidebarWidth = 520;
|
|
12
|
+
export const DefaultSidebarWidth = 256;
|
|
13
|
+
|
|
14
|
+
const storageKey = "intel.sidebar";
|
|
15
|
+
const defaults: SidebarPreferences = { open: true, width: DefaultSidebarWidth };
|
|
16
|
+
|
|
17
|
+
export function clampSidebarWidth(width: number): number {
|
|
18
|
+
if (!Number.isFinite(width)) return DefaultSidebarWidth;
|
|
19
|
+
return Math.min(MaxSidebarWidth, Math.max(MinSidebarWidth, Math.round(width)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function browserStorage(): Pick<Storage, "getItem" | "setItem"> | null {
|
|
23
|
+
// Reading `localStorage` throws outright in a few privacy modes, so the guard is a try, not a
|
|
24
|
+
// typeof check. Losing the preference is acceptable; losing the shell is not.
|
|
25
|
+
try {
|
|
26
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The shell's own client state: not server data, so it stays out of TanStack Query, but it has to
|
|
33
|
+
// survive a page change — which in a SPA includes a full reload — so it is written through.
|
|
34
|
+
export function createSidebarPreferences(deps: SidebarPreferenceDeps = {}): SidebarPreferenceStore {
|
|
35
|
+
const storage = deps.storage === undefined ? browserStorage() : deps.storage;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
read() {
|
|
39
|
+
let raw: string | null = null;
|
|
40
|
+
try {
|
|
41
|
+
raw = storage?.getItem(storageKey) ?? null;
|
|
42
|
+
} catch {
|
|
43
|
+
return defaults;
|
|
44
|
+
}
|
|
45
|
+
if (raw === null) return defaults;
|
|
46
|
+
let parsed: unknown;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(raw);
|
|
49
|
+
} catch {
|
|
50
|
+
return defaults;
|
|
51
|
+
}
|
|
52
|
+
if (typeof parsed !== "object" || parsed === null) return defaults;
|
|
53
|
+
const value = parsed as Partial<SidebarPreferences>;
|
|
54
|
+
return {
|
|
55
|
+
open: typeof value.open === "boolean" ? value.open : defaults.open,
|
|
56
|
+
width: clampSidebarWidth(typeof value.width === "number" ? value.width : defaults.width),
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
write(preferences) {
|
|
60
|
+
try {
|
|
61
|
+
storage?.setItem(
|
|
62
|
+
storageKey,
|
|
63
|
+
JSON.stringify({ open: preferences.open, width: clampSidebarWidth(preferences.width) }),
|
|
64
|
+
);
|
|
65
|
+
} catch {}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|