@shortlink-org/portolan 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +22 -4
  36. package/scripts/local-api.test.mjs +84 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +6 -2
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +12 -8
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -7,12 +7,12 @@
7
7
 
8
8
  import { useEffect, useMemo, useRef } from "react";
9
9
  import type { MouseEvent as ReactMouseEvent } from "react";
10
- import { useNavigate } from "react-router";
10
+ import { useLocation, useNavigate } from "react-router";
11
11
  import { Chat, useChat } from "@ai-sdk/react";
12
12
  import { getToolName, isToolUIPart } from "ai";
13
13
  import type { ChatStatus, UIMessage } from "ai";
14
14
  import { Markdown } from "../components/Markdown";
15
- import { catalog } from "../data";
15
+ import { activeCatalogProfile, catalog } from "../data";
16
16
  import { paletteItems } from "../lib/palette";
17
17
  import { ToolCard, toolOutput } from "./Cards";
18
18
  import { Composer } from "./Composer";
@@ -30,10 +30,12 @@ import type { Read } from "./Steps";
30
30
  import { useChatUi } from "./store";
31
31
  import { transportFor } from "./transport";
32
32
  import { Sprite } from "./Waiting";
33
+ import { pageContext } from "./page-context";
33
34
 
34
35
  export type Answering = Extract<ChatRoute, { kind: "proxy" | "own" }>;
35
36
 
36
37
  const base = import.meta.env.BASE_URL;
38
+ const stayHere = () => {};
37
39
 
38
40
  /** Every id the catalog has a page for, and the page. */
39
41
  const LINKS: ReadonlyMap<string, string> = (() => {
@@ -91,7 +93,8 @@ function waitingLabel(
91
93
  if (!last || last.role !== "assistant") return "thinking…";
92
94
  const part = last.parts[last.parts.length - 1];
93
95
  if (!part) return "thinking…";
94
- if (part.type === "text") return part.state === "streaming" ? null : "thinking…";
96
+ if (part.type === "text")
97
+ return part.state === "streaming" ? null : "thinking…";
95
98
  if (isToolUIPart(part) && getToolName(part) === "read_page") {
96
99
  if (part.state === "input-streaming" || part.state === "input-available") {
97
100
  return `reading ${pathOf(part.input) ?? "a page"}`;
@@ -138,7 +141,12 @@ function Answer({
138
141
  }
139
142
  if (isDisplayTool(name) && part.state !== "input-streaming") {
140
143
  return (
141
- <ToolCard key={index} name={name} input={part.input} onNavigate={onNavigate} />
144
+ <ToolCard
145
+ key={index}
146
+ name={name}
147
+ input={part.input}
148
+ onNavigate={onNavigate}
149
+ />
142
150
  );
143
151
  }
144
152
  return null;
@@ -147,16 +155,41 @@ function Answer({
147
155
  );
148
156
  }
149
157
 
158
+ function SeededExchange({ onNavigate }: { onNavigate: () => void }) {
159
+ return (
160
+ <>
161
+ <Message role="user">Show the “Checkout” flow.</Message>
162
+ <Message role="assistant" foot="grounded in flows/cart-checkout.md">
163
+ <ReadSteps reads={[{ path: "flows/cart-checkout.md" }]} />
164
+ <p className="leading-6 text-muted">
165
+ <span className="font-medium text-ink">shop.cart</span> validates the
166
+ session with auth.auth, loads the basket, requests its total from
167
+ shop.pricing, saves it, then publishes BasketCheckedOut.
168
+ </p>
169
+ <ToolCard
170
+ name="show_flow"
171
+ input={{ id: "flow.cart-checkout" }}
172
+ onNavigate={onNavigate}
173
+ />
174
+ </Message>
175
+ </>
176
+ );
177
+ }
178
+
150
179
  export function Conversation({
151
180
  route,
152
181
  onOwnKey,
153
182
  onSettings,
154
183
  onClose,
184
+ seeded = false,
155
185
  }: {
156
186
  route: Answering;
157
187
  onOwnKey: () => void;
158
188
  onSettings: () => void;
159
- onClose: () => void;
189
+ /** Omitted when the conversation is embedded in a page. */
190
+ onClose?: () => void;
191
+ /** Show one useful, catalog-grounded exchange before the reader asks. */
192
+ seeded?: boolean;
160
193
  }) {
161
194
  const chat = useMemo(() => chatFor(route), [route]);
162
195
  const {
@@ -173,6 +206,8 @@ export function Conversation({
173
206
  const phase = useChatUi((s) => s.phase);
174
207
  const took = useChatUi((s) => s.took);
175
208
  const navigate = useNavigate();
209
+ const location = useLocation();
210
+ const page = useMemo(() => pageContext(location.pathname), [location.pathname]);
176
211
  const list = useRef<HTMLDivElement>(null);
177
212
  const busy = status === "submitted" || status === "streaming";
178
213
  const waiting = waitingLabel(messages, status, phase);
@@ -205,7 +240,15 @@ export function Conversation({
205
240
  const send = (question: string) => {
206
241
  if (busy) return;
207
242
  useChatUi.getState().setAskedAt(Date.now());
208
- void sendMessage({ text: question });
243
+ void sendMessage(
244
+ { text: question },
245
+ {
246
+ body: {
247
+ catalogId: activeCatalogProfile.id,
248
+ ...(page ? { pageContext: page } : {}),
249
+ },
250
+ },
251
+ );
209
252
  };
210
253
 
211
254
  // Links in an answer point into the app; follow them without a reload, and
@@ -218,16 +261,19 @@ export function Conversation({
218
261
  if (!href.startsWith(base) || /^[a-z]+:/i.test(href)) return;
219
262
  event.preventDefault();
220
263
  navigate(`/${href.slice(base.length)}`);
221
- onClose();
264
+ onClose?.();
222
265
  };
223
266
 
224
267
  return (
225
268
  <>
226
269
  <Header
227
270
  route={route}
271
+ page={page}
228
272
  onSettings={onSettings}
229
- onClose={onClose}
230
- {...(messages.length > 0 && !busy ? { onClear: () => setMessages([]) } : {})}
273
+ {...(onClose ? { onClose } : {})}
274
+ {...(messages.length > 0 && !busy
275
+ ? { onClear: () => setMessages([]) }
276
+ : {})}
231
277
  />
232
278
  <div
233
279
  ref={list}
@@ -236,19 +282,27 @@ export function Conversation({
236
282
  aria-live="polite"
237
283
  className="min-h-0 flex-1 space-y-5 overflow-y-auto px-4 py-4"
238
284
  >
239
- {messages.length === 0 ? <Starter onAsk={send} /> : null}
285
+ {seeded ? (
286
+ <SeededExchange onNavigate={onClose ?? stayHere} />
287
+ ) : messages.length === 0 ? (
288
+ <Starter onAsk={send} page={page} />
289
+ ) : null}
240
290
  {messages.map((message) =>
241
291
  message.role === "user" ? (
242
292
  <Message key={message.id} role="user">
243
- {message.parts.map((part) => (part.type === "text" ? part.text : "")).join("")}
293
+ {message.parts
294
+ .map((part) => (part.type === "text" ? part.text : ""))
295
+ .join("")}
244
296
  </Message>
245
297
  ) : message.role === "assistant" ? (
246
298
  <Message
247
299
  key={message.id}
248
300
  role="assistant"
249
- {...(took[message.id] !== undefined ? { foot: `took ${took[message.id]}s` } : {})}
301
+ {...(took[message.id] !== undefined
302
+ ? { foot: `took ${took[message.id]}s` }
303
+ : {})}
250
304
  >
251
- <Answer message={message} onNavigate={onClose} />
305
+ <Answer message={message} onNavigate={onClose ?? stayHere} />
252
306
  </Message>
253
307
  ) : null,
254
308
  )}
@@ -268,14 +322,24 @@ export function Conversation({
268
322
  onRetry={() => {
269
323
  clearError();
270
324
  useChatUi.getState().setAskedAt(Date.now());
271
- void regenerate();
325
+ void regenerate({
326
+ body: {
327
+ catalogId: activeCatalogProfile.id,
328
+ ...(page ? { pageContext: page } : {}),
329
+ },
330
+ });
272
331
  }}
273
332
  onOwnKey={onOwnKey}
274
333
  />
275
334
  </div>
276
335
  ) : null}
277
336
  </div>
278
- <Composer busy={busy} onSend={send} onStop={() => void stop()} />
337
+ <Composer
338
+ busy={busy}
339
+ onSend={send}
340
+ onStop={() => void stop()}
341
+ focusOnMount={!seeded}
342
+ />
279
343
  </>
280
344
  );
281
345
  }
@@ -3,6 +3,7 @@
3
3
  import { Eraser, MessageSquare, Settings2, X } from "lucide-react";
4
4
  import { routeLabel } from "./flags";
5
5
  import type { ChatRoute } from "./flags";
6
+ import type { ChatPageContext } from "./page-context";
6
7
 
7
8
  const ICON =
8
9
  "flex size-8 items-center justify-center rounded-control text-muted t-micro transition-colors hover:bg-surface hover:text-ink";
@@ -12,34 +13,67 @@ export function Header({
12
13
  onSettings,
13
14
  onClear,
14
15
  onClose,
16
+ page,
15
17
  }: {
16
18
  route: ChatRoute;
19
+ page?: ChatPageContext | null;
17
20
  onSettings: () => void;
18
21
  /** Present once there is a conversation to start over from. */
19
22
  onClear?: () => void;
20
- onClose: () => void;
23
+ /** Omitted when the conversation is embedded in a page. */
24
+ onClose?: () => void;
21
25
  }) {
22
26
  return (
23
27
  <div className="flex items-center gap-2.5 border-b border-line px-4 py-2.5">
24
28
  <MessageSquare size={16} aria-hidden className="shrink-0 text-accent" />
25
29
  <div className="min-w-0">
26
- <div className="font-semibold leading-tight text-ink">ask the catalog</div>
27
- <div className="mono truncate text-muted">
28
- {routeLabel(route) || "nothing set to answer"}
30
+ <div className="font-semibold leading-tight text-ink">
31
+ ask the catalog
32
+ </div>
33
+ <div className="mono flex min-w-0 items-center gap-1.5 truncate text-muted">
34
+ <span className="truncate">{routeLabel(route) || "nothing set to answer"}</span>
35
+ {page ? (
36
+ <>
37
+ <span aria-hidden className="text-faint">·</span>
38
+ <span className="truncate text-ink" title={`${page.kind} ${page.id}`}>
39
+ {page.kind} · {page.title}
40
+ </span>
41
+ </>
42
+ ) : null}
29
43
  </div>
30
44
  </div>
31
45
  <div className="ml-auto flex shrink-0 items-center">
32
46
  {onClear ? (
33
- <button type="button" onClick={onClear} aria-label="New conversation" title="New conversation" className={ICON}>
47
+ <button
48
+ type="button"
49
+ onClick={onClear}
50
+ aria-label="New conversation"
51
+ title="New conversation"
52
+ className={ICON}
53
+ >
34
54
  <Eraser size={15} aria-hidden />
35
55
  </button>
36
56
  ) : null}
37
- <button type="button" onClick={onSettings} aria-label="Model settings" title="Model settings" className={ICON}>
57
+ <button
58
+ type="button"
59
+ onClick={onSettings}
60
+ aria-label="Model settings"
61
+ title="Model settings"
62
+ className={ICON}
63
+ >
38
64
  <Settings2 size={15} aria-hidden />
39
65
  </button>
40
- <button type="button" onClick={onClose} aria-label="Close" title="Close — Esc" className={ICON}>
41
- <X size={15} aria-hidden />
42
- </button>
66
+ {onClose ? (
67
+ <button
68
+ type="button"
69
+ onClick={onClose}
70
+ aria-label="Close"
71
+ title="Close — Esc"
72
+ className={ICON}
73
+ >
74
+ <X size={15} aria-hidden />
75
+ </button>
76
+ ) : null}
43
77
  </div>
44
78
  </div>
45
79
  );
@@ -6,7 +6,9 @@
6
6
 
7
7
  import { KindIcon } from "../components/kind";
8
8
  import type { Kind } from "../lib/kinds";
9
- import { catalog } from "../data";
9
+ import { activeCatalogProfile, catalog } from "../data";
10
+ import type { ChatPageContext } from "./page-context";
11
+ import { contextQuestions } from "./page-context";
10
12
 
11
13
  interface Example {
12
14
  kind: Kind;
@@ -34,7 +36,26 @@ function examples(): Example[] {
34
36
  return out;
35
37
  }
36
38
 
37
- export function Starter({ onAsk }: { onAsk: (question: string) => void }) {
39
+ const base = import.meta.env.BASE_URL;
40
+
41
+ export function Starter({
42
+ onAsk,
43
+ page,
44
+ }: {
45
+ onAsk: (question: string) => void;
46
+ page?: ChatPageContext | null;
47
+ }) {
48
+ const choices: Example[] = page
49
+ ? contextQuestions(page).map((question) => ({
50
+ kind: page.kind,
51
+ question,
52
+ }))
53
+ : examples();
54
+ const profileSuffix =
55
+ activeCatalogProfile.id === "portolan" ? "" : `${activeCatalogProfile.id}/`;
56
+ const docsRoot = `${base}docs/${profileSuffix}`;
57
+ const indexHref = profileSuffix ? `${docsRoot}llms.txt` : `${base}llms.txt`;
58
+
38
59
  return (
39
60
  <div className="pt-1">
40
61
  <p className="text-muted">
@@ -42,9 +63,49 @@ export function Starter({ onAsk }: { onAsk: (question: string) => void }) {
42
63
  link. Ask about a service, a flow, a decision, or what stands between two
43
64
  contexts.
44
65
  </p>
66
+ {page ? (
67
+ <div className="mt-4 rounded-card border border-line bg-surface px-3 py-2.5 shadow-xs">
68
+ <div className="flex min-w-0 items-start gap-2.5">
69
+ <KindIcon
70
+ kind={page.kind}
71
+ size={16}
72
+ className="mt-0.5 shrink-0"
73
+ />
74
+ <div className="min-w-0 flex-1">
75
+ <div className="label">current page</div>
76
+ <div className="mt-0.5 truncate font-medium text-ink" title={page.title}>
77
+ {page.title}
78
+ </div>
79
+ <div className="mono truncate text-muted" title={page.id}>{page.id}</div>
80
+ </div>
81
+ </div>
82
+ <div className="mono mt-2.5 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-line pt-2 text-muted">
83
+ <span className="text-faint">model context</span>
84
+ <a
85
+ href={indexHref}
86
+ target="_blank"
87
+ rel="noreferrer"
88
+ className="rounded-control text-accent hover:underline"
89
+ >
90
+ llms.txt
91
+ </a>
92
+ {page.docPath ? (
93
+ <a
94
+ href={`${docsRoot}${page.docPath}`}
95
+ target="_blank"
96
+ rel="noreferrer"
97
+ className="max-w-full truncate rounded-control text-accent hover:underline"
98
+ title={page.docPath}
99
+ >
100
+ current catalog page
101
+ </a>
102
+ ) : null}
103
+ </div>
104
+ </div>
105
+ ) : null}
45
106
  <div className="label mt-5 mb-1">try one</div>
46
107
  <ul className="-mx-1">
47
- {examples().map((example) => (
108
+ {choices.map((example) => (
48
109
  <li key={example.question}>
49
110
  <button
50
111
  type="button"
@@ -24,7 +24,7 @@ export function ReadSteps({ reads }: { reads: Read[] }) {
24
24
  className="transition-transform group-open:rotate-90"
25
25
  />
26
26
  <FileText size={13} aria-hidden />
27
- read {reads.length} {reads.length === 1 ? "page" : "pages"}
27
+ sources · {reads.length} {reads.length === 1 ? "page" : "pages"}
28
28
  </summary>
29
29
  <ul className="mono mt-1 space-y-0.5 pl-6">
30
30
  {reads.map((read, index) => (
@@ -2,18 +2,32 @@ import { describe, expect, it } from "vitest";
2
2
  import { chatRoute, ownReady, parsePrefs, routeLabel } from "./flags";
3
3
  import type { ChatBuild, OwnModel } from "./flags";
4
4
 
5
- const withProxy: ChatBuild = { built: true, proxyUrl: "https://x.workers.dev/chat" };
5
+ const withProxy: ChatBuild = {
6
+ built: true,
7
+ proxyUrl: "https://x.workers.dev/chat",
8
+ };
6
9
  const noProxy: ChatBuild = { built: true, proxyUrl: "" };
7
- const notBuilt: ChatBuild = { built: false, proxyUrl: "https://x.workers.dev/chat" };
8
- const own: OwnModel = { baseUrl: "https://api.groq.com/openai/v1", apiKey: "k", model: "llama" };
10
+ const notBuilt: ChatBuild = {
11
+ built: false,
12
+ proxyUrl: "https://x.workers.dev/chat",
13
+ };
14
+ const own: OwnModel = {
15
+ baseUrl: "https://api.groq.com/openai/v1",
16
+ apiKey: "k",
17
+ model: "llama",
18
+ };
9
19
 
10
20
  describe("chatRoute", () => {
11
21
  it("is off when the build has no chat, whatever the reader set", () => {
12
- expect(chatRoute(notBuilt, { enabled: true, own })).toEqual({ kind: "off" });
22
+ expect(chatRoute(notBuilt, { enabled: true, own })).toEqual({
23
+ kind: "off",
24
+ });
13
25
  });
14
26
 
15
27
  it("is off when the reader switched it off, even with a proxy and a key", () => {
16
- expect(chatRoute(withProxy, { enabled: false, own })).toEqual({ kind: "off" });
28
+ expect(chatRoute(withProxy, { enabled: false, own })).toEqual({
29
+ kind: "off",
30
+ });
17
31
  });
18
32
 
19
33
  it("uses the proxy by default when the build has one", () => {
@@ -24,25 +38,43 @@ describe("chatRoute", () => {
24
38
  });
25
39
 
26
40
  it("prefers the reader's own model over the proxy", () => {
27
- expect(chatRoute(withProxy, { enabled: null, own })).toEqual({ kind: "own", model: own });
41
+ expect(chatRoute(withProxy, { enabled: null, own })).toEqual({
42
+ kind: "own",
43
+ model: own,
44
+ });
28
45
  });
29
46
 
30
47
  it("stays off without a proxy until switched on, then asks to be configured", () => {
31
- expect(chatRoute(noProxy, { enabled: null, own: null })).toEqual({ kind: "off" });
32
- expect(chatRoute(noProxy, { enabled: true, own: null })).toEqual({ kind: "unconfigured" });
48
+ expect(chatRoute(noProxy, { enabled: null, own: null })).toEqual({
49
+ kind: "off",
50
+ });
51
+ expect(chatRoute(noProxy, { enabled: true, own: null })).toEqual({
52
+ kind: "unconfigured",
53
+ });
33
54
  });
34
55
 
35
56
  it("does not count a half-filled model as one", () => {
36
57
  expect(ownReady({ baseUrl: "", apiKey: "k", model: "m" })).toBe(false);
37
- expect(ownReady({ baseUrl: "https://a", apiKey: "", model: "m" })).toBe(true);
38
- expect(chatRoute(noProxy, { enabled: true, own: { baseUrl: "https://a", apiKey: "", model: " " } })).toEqual({ kind: "unconfigured" });
58
+ expect(ownReady({ baseUrl: "https://a", apiKey: "", model: "m" })).toBe(
59
+ true,
60
+ );
61
+ expect(
62
+ chatRoute(noProxy, {
63
+ enabled: true,
64
+ own: { baseUrl: "https://a", apiKey: "", model: " " },
65
+ }),
66
+ ).toEqual({ kind: "unconfigured" });
39
67
  });
40
68
  });
41
69
 
42
70
  describe("routeLabel", () => {
43
71
  it("names the model and the host, or the proxy", () => {
44
- expect(routeLabel({ kind: "own", model: own })).toBe("llama · api.groq.com");
45
- expect(routeLabel({ kind: "proxy", url: "u" })).toBe("demo proxy");
72
+ expect(routeLabel({ kind: "own", model: own })).toBe(
73
+ "llama · api.groq.com",
74
+ );
75
+ expect(routeLabel({ kind: "proxy", url: "u" })).toBe(
76
+ "default model · demo proxy",
77
+ );
46
78
  expect(routeLabel({ kind: "off" })).toBe("");
47
79
  });
48
80
  });
@@ -50,8 +82,13 @@ describe("routeLabel", () => {
50
82
  describe("parsePrefs", () => {
51
83
  it("reads the switch and the model, and shrugs at junk", () => {
52
84
  expect(parsePrefs("on", null)).toEqual({ enabled: true, own: null });
53
- expect(parsePrefs("off", "{bad json")).toEqual({ enabled: false, own: null });
54
- expect(parsePrefs(null, JSON.stringify({ baseUrl: "https://a", model: "m" }))).toEqual({
85
+ expect(parsePrefs("off", "{bad json")).toEqual({
86
+ enabled: false,
87
+ own: null,
88
+ });
89
+ expect(
90
+ parsePrefs(null, JSON.stringify({ baseUrl: "https://a", model: "m" })),
91
+ ).toEqual({
55
92
  enabled: null,
56
93
  own: { baseUrl: "https://a", apiKey: "", model: "m" },
57
94
  });
package/src/chat/flags.ts CHANGED
@@ -39,7 +39,13 @@ export type ChatRoute =
39
39
 
40
40
  export const BUILD: ChatBuild = {
41
41
  built: import.meta.env.VITE_CHAT !== "off",
42
- proxyUrl: (import.meta.env.VITE_CHAT_PROXY_URL ?? "").trim(),
42
+ // The public build receives the worker URL from CI. In development Vite
43
+ // exposes the same worker through a same-origin proxy, so the real demo chat
44
+ // works on any local port without weakening the worker's CORS allowlist.
45
+ proxyUrl: (
46
+ import.meta.env.VITE_CHAT_PROXY_URL ??
47
+ (import.meta.env.DEV ? "/api/portolan-chat" : "")
48
+ ).trim(),
43
49
  };
44
50
 
45
51
  const OFF: ChatRoute = { kind: "off" };
@@ -66,7 +72,7 @@ export function chatRoute(build: ChatBuild, prefs: ChatPrefs): ChatRoute {
66
72
  export function routeLabel(route: ChatRoute): string {
67
73
  switch (route.kind) {
68
74
  case "proxy":
69
- return "demo proxy";
75
+ return "default model · demo proxy";
70
76
  case "own": {
71
77
  let host = route.model.baseUrl;
72
78
  try {
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { PaletteItem } from "../lib/palette";
3
+ import { contextQuestions, pageContextFrom } from "./page-context";
4
+
5
+ const flow: PaletteItem = {
6
+ kind: "flow",
7
+ id: "flow.cart-checkout",
8
+ name: "cart-checkout",
9
+ detail: "Checkout",
10
+ path: "/flows/cart-checkout",
11
+ context: null,
12
+ };
13
+
14
+ const service: PaletteItem = {
15
+ kind: "service",
16
+ id: "shop.cart",
17
+ name: "cart",
18
+ detail: "shop.cart",
19
+ path: "/c/shop/cart",
20
+ context: "shop",
21
+ };
22
+
23
+ describe("pageContextFrom", () => {
24
+ it("turns the exact entity route into model context", () => {
25
+ expect(pageContextFrom([flow], "/flows/cart-checkout")).toEqual({
26
+ kind: "flow",
27
+ id: "flow.cart-checkout",
28
+ title: "Checkout",
29
+ docPath: "flows/cart-checkout.md",
30
+ });
31
+ });
32
+
33
+ it("does not mistake an index or nested route for the entity", () => {
34
+ expect(pageContextFrom([flow], "/flows")).toBeNull();
35
+ expect(pageContextFrom([flow], "/flows/cart-checkout/extra")).toBeNull();
36
+ });
37
+
38
+ it("points a service at the markdown path from llms.txt", () => {
39
+ expect(pageContextFrom([service], "/c/shop/cart")?.docPath).toBe(
40
+ "shop/cart/README.md",
41
+ );
42
+ });
43
+ });
44
+
45
+ describe("contextQuestions", () => {
46
+ it("keeps every flow starter anchored to the catalog id", () => {
47
+ const page = pageContextFrom([flow], "/flows/cart-checkout");
48
+ expect(page).not.toBeNull();
49
+ expect(contextQuestions(page!).every((question) => question.includes(flow.id))).toBe(true);
50
+ });
51
+ });