@skillcdn/console 0.4.1 → 0.4.3

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 (299) hide show
  1. package/README.md +19 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +5 -4
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +5 -4
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +569 -48
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +34 -3
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +283 -7
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +44 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +26 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +396 -0
  20. package/dist/cli/plugin-check.js.map +1 -0
  21. package/dist/cli/plugin-cli.d.ts +20 -0
  22. package/dist/cli/plugin-cli.d.ts.map +1 -0
  23. package/dist/cli/plugin-cli.js +189 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +37 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +206 -0
  28. package/dist/cli/plugin-init.js.map +1 -0
  29. package/dist/cli/plugin-main.d.ts +3 -0
  30. package/dist/cli/plugin-main.d.ts.map +1 -0
  31. package/dist/cli/plugin-main.js +28 -0
  32. package/dist/cli/plugin-main.js.map +1 -0
  33. package/dist/client.d.ts +100 -3
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +79 -7
  36. package/dist/client.js.map +1 -1
  37. package/dist/compat.d.ts +1 -1
  38. package/dist/compat.js +2 -2
  39. package/dist/components/board.d.ts +8 -0
  40. package/dist/components/board.d.ts.map +1 -1
  41. package/dist/components/board.js +59 -15
  42. package/dist/components/board.js.map +1 -1
  43. package/dist/components/comments.d.ts.map +1 -1
  44. package/dist/components/comments.js +1 -1
  45. package/dist/components/comments.js.map +1 -1
  46. package/dist/components/decision-form.d.ts +2 -1
  47. package/dist/components/decision-form.d.ts.map +1 -1
  48. package/dist/components/decision-form.js +36 -4
  49. package/dist/components/decision-form.js.map +1 -1
  50. package/dist/components/decision-list.d.ts +4 -0
  51. package/dist/components/decision-list.d.ts.map +1 -1
  52. package/dist/components/decision-list.js +29 -3
  53. package/dist/components/decision-list.js.map +1 -1
  54. package/dist/components/event-feed.d.ts +1 -1
  55. package/dist/components/event-feed.d.ts.map +1 -1
  56. package/dist/components/event-feed.js +33 -1
  57. package/dist/components/event-feed.js.map +1 -1
  58. package/dist/components/my-decisions.d.ts +2 -0
  59. package/dist/components/my-decisions.d.ts.map +1 -1
  60. package/dist/components/my-decisions.js +2 -2
  61. package/dist/components/my-decisions.js.map +1 -1
  62. package/dist/components/plugin-frame.d.ts +52 -0
  63. package/dist/components/plugin-frame.d.ts.map +1 -0
  64. package/dist/components/plugin-frame.js +314 -0
  65. package/dist/components/plugin-frame.js.map +1 -0
  66. package/dist/components/plugin-settings.d.ts +38 -0
  67. package/dist/components/plugin-settings.d.ts.map +1 -0
  68. package/dist/components/plugin-settings.js +348 -0
  69. package/dist/components/plugin-settings.js.map +1 -0
  70. package/dist/components/shell.d.ts +2 -0
  71. package/dist/components/shell.d.ts.map +1 -1
  72. package/dist/components/shell.js +26 -1
  73. package/dist/components/shell.js.map +1 -1
  74. package/dist/components/task-view.d.ts +2 -1
  75. package/dist/components/task-view.d.ts.map +1 -1
  76. package/dist/components/task-view.js.map +1 -1
  77. package/dist/components/workspace.d.ts +7 -1
  78. package/dist/components/workspace.d.ts.map +1 -1
  79. package/dist/components/workspace.js +1 -1
  80. package/dist/components/workspace.js.map +1 -1
  81. package/dist/console.css +330 -1
  82. package/dist/console.d.ts.map +1 -1
  83. package/dist/console.js +152 -41
  84. package/dist/console.js.map +1 -1
  85. package/dist/data.d.ts +9 -2
  86. package/dist/data.d.ts.map +1 -1
  87. package/dist/data.js +45 -9
  88. package/dist/data.js.map +1 -1
  89. package/dist/i18n/en.d.ts +155 -0
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +166 -0
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/ko.d.ts.map +1 -1
  94. package/dist/i18n/ko.js +150 -0
  95. package/dist/i18n/ko.js.map +1 -1
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +3 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/limits.d.ts +2 -0
  101. package/dist/limits.d.ts.map +1 -1
  102. package/dist/limits.js +2 -0
  103. package/dist/limits.js.map +1 -1
  104. package/dist/plugin/args.d.ts +148 -0
  105. package/dist/plugin/args.d.ts.map +1 -0
  106. package/dist/plugin/args.js +369 -0
  107. package/dist/plugin/args.js.map +1 -0
  108. package/dist/plugin/bridge.d.ts +146 -0
  109. package/dist/plugin/bridge.d.ts.map +1 -0
  110. package/dist/plugin/bridge.js +125 -0
  111. package/dist/plugin/bridge.js.map +1 -0
  112. package/dist/plugin/capabilities.d.ts +20 -0
  113. package/dist/plugin/capabilities.d.ts.map +1 -0
  114. package/dist/plugin/capabilities.js +133 -0
  115. package/dist/plugin/capabilities.js.map +1 -0
  116. package/dist/plugin/context.d.ts +380 -0
  117. package/dist/plugin/context.d.ts.map +1 -0
  118. package/dist/plugin/context.js +2 -0
  119. package/dist/plugin/context.js.map +1 -0
  120. package/dist/plugin/define.d.ts +208 -0
  121. package/dist/plugin/define.d.ts.map +1 -0
  122. package/dist/plugin/define.js +482 -0
  123. package/dist/plugin/define.js.map +1 -0
  124. package/dist/plugin/definition-error.d.ts +11 -0
  125. package/dist/plugin/definition-error.d.ts.map +1 -0
  126. package/dist/plugin/definition-error.js +17 -0
  127. package/dist/plugin/definition-error.js.map +1 -0
  128. package/dist/plugin/limits.d.ts +67 -0
  129. package/dist/plugin/limits.d.ts.map +1 -0
  130. package/dist/plugin/limits.js +70 -0
  131. package/dist/plugin/limits.js.map +1 -0
  132. package/dist/plugin/load-worker.d.ts +7 -0
  133. package/dist/plugin/load-worker.d.ts.map +1 -0
  134. package/dist/plugin/load-worker.js +41 -0
  135. package/dist/plugin/load-worker.js.map +1 -0
  136. package/dist/plugin/load.d.ts +18 -0
  137. package/dist/plugin/load.d.ts.map +1 -0
  138. package/dist/plugin/load.js +62 -0
  139. package/dist/plugin/load.js.map +1 -0
  140. package/dist/plugin/manifest.d.ts +237 -0
  141. package/dist/plugin/manifest.d.ts.map +1 -0
  142. package/dist/plugin/manifest.js +411 -0
  143. package/dist/plugin/manifest.js.map +1 -0
  144. package/dist/plugin/refusals.d.ts +49 -0
  145. package/dist/plugin/refusals.d.ts.map +1 -0
  146. package/dist/plugin/refusals.js +79 -0
  147. package/dist/plugin/refusals.js.map +1 -0
  148. package/dist/plugin/rest-schemas.d.ts +556 -0
  149. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  150. package/dist/plugin/rest-schemas.js +262 -0
  151. package/dist/plugin/rest-schemas.js.map +1 -0
  152. package/dist/plugin/sandbox.d.ts +23 -0
  153. package/dist/plugin/sandbox.d.ts.map +1 -0
  154. package/dist/plugin/sandbox.js +99 -0
  155. package/dist/plugin/sandbox.js.map +1 -0
  156. package/dist/plugin/testing.d.ts +45 -0
  157. package/dist/plugin/testing.d.ts.map +1 -0
  158. package/dist/plugin/testing.js +420 -0
  159. package/dist/plugin/testing.js.map +1 -0
  160. package/dist/plugin/web-react.d.ts +11 -0
  161. package/dist/plugin/web-react.d.ts.map +1 -0
  162. package/dist/plugin/web-react.js +34 -0
  163. package/dist/plugin/web-react.js.map +1 -0
  164. package/dist/plugin/web.d.ts +95 -0
  165. package/dist/plugin/web.d.ts.map +1 -0
  166. package/dist/plugin/web.js +245 -0
  167. package/dist/plugin/web.js.map +1 -0
  168. package/dist/plugin-nav.d.ts +29 -0
  169. package/dist/plugin-nav.d.ts.map +1 -0
  170. package/dist/plugin-nav.js +92 -0
  171. package/dist/plugin-nav.js.map +1 -0
  172. package/dist/plugin-testing.d.ts +2 -0
  173. package/dist/plugin-testing.d.ts.map +1 -0
  174. package/dist/plugin-testing.js +4 -0
  175. package/dist/plugin-testing.js.map +1 -0
  176. package/dist/plugin-web-react.d.ts +3 -0
  177. package/dist/plugin-web-react.d.ts.map +1 -0
  178. package/dist/plugin-web-react.js +3 -0
  179. package/dist/plugin-web-react.js.map +1 -0
  180. package/dist/plugin-web.d.ts +4 -0
  181. package/dist/plugin-web.d.ts.map +1 -0
  182. package/dist/plugin-web.js +5 -0
  183. package/dist/plugin-web.js.map +1 -0
  184. package/dist/plugin.d.ts +11 -0
  185. package/dist/plugin.d.ts.map +1 -0
  186. package/dist/plugin.js +15 -0
  187. package/dist/plugin.js.map +1 -0
  188. package/dist/router.d.ts +28 -0
  189. package/dist/router.d.ts.map +1 -1
  190. package/dist/router.js +47 -0
  191. package/dist/router.js.map +1 -1
  192. package/dist/routes.d.ts +4 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +6 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/schemas.d.ts +194 -9
  197. package/dist/schemas.d.ts.map +1 -1
  198. package/dist/schemas.js +98 -17
  199. package/dist/schemas.js.map +1 -1
  200. package/dist/tokens.d.ts +2 -0
  201. package/dist/tokens.d.ts.map +1 -0
  202. package/dist/tokens.js +101 -0
  203. package/dist/tokens.js.map +1 -0
  204. package/dist/vocabulary.d.ts +11 -1
  205. package/dist/vocabulary.d.ts.map +1 -1
  206. package/dist/vocabulary.js +24 -0
  207. package/dist/vocabulary.js.map +1 -1
  208. package/dist/web.d.ts.map +1 -1
  209. package/dist/web.js +2 -0
  210. package/dist/web.js.map +1 -1
  211. package/docs/specs/cli.md +12 -5
  212. package/docs/specs/plugins.md +488 -0
  213. package/docs/specs/rest.md +11 -9
  214. package/package.json +26 -8
  215. package/skills/building-a-console/SKILL.md +25 -26
  216. package/skills/building-a-console/references/configuring.md +40 -0
  217. package/skills/building-a-console/references/the-default-console.md +1 -1
  218. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  219. package/skills/building-a-plugin/SKILL.md +137 -0
  220. package/skills/deploying-a-console/SKILL.md +24 -26
  221. package/skills/deploying-a-console/references/access.md +13 -9
  222. package/skills/deploying-a-console/references/resources.md +25 -10
  223. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  224. package/skills/deploying-a-console/references/sizing.md +2 -0
  225. package/skills/planning-the-board/SKILL.md +96 -0
  226. package/skills/planning-the-board/references/a-task.md +36 -0
  227. package/skills/planning-the-board/references/setting-up.md +44 -0
  228. package/skills/working-the-board/SKILL.md +43 -34
  229. package/skills/working-the-board/references/the-pack.md +19 -0
  230. package/src/api.ts +12 -0
  231. package/src/cli/cli.ts +737 -55
  232. package/src/cli/format.ts +368 -10
  233. package/src/cli/help.ts +50 -6
  234. package/src/cli/plugin-check.ts +497 -0
  235. package/src/cli/plugin-cli.ts +237 -0
  236. package/src/cli/plugin-init.ts +257 -0
  237. package/src/cli/plugin-main.ts +30 -0
  238. package/src/client.ts +322 -9
  239. package/src/compat.ts +2 -2
  240. package/src/components/board.tsx +62 -14
  241. package/src/components/comments.tsx +1 -0
  242. package/src/components/decision-form.tsx +92 -11
  243. package/src/components/decision-list.tsx +71 -2
  244. package/src/components/event-feed.tsx +42 -1
  245. package/src/components/my-decisions.tsx +4 -1
  246. package/src/components/plugin-frame.tsx +437 -0
  247. package/src/components/plugin-settings.tsx +1111 -0
  248. package/src/components/shell.tsx +82 -14
  249. package/src/components/task-view.tsx +2 -1
  250. package/src/components/workspace.tsx +18 -0
  251. package/src/console.tsx +304 -31
  252. package/src/data.ts +59 -11
  253. package/src/i18n/en.ts +175 -0
  254. package/src/i18n/ko.ts +155 -0
  255. package/src/index.ts +12 -0
  256. package/src/limits.ts +2 -0
  257. package/src/plugin/args.ts +526 -0
  258. package/src/plugin/bridge.ts +314 -0
  259. package/src/plugin/capabilities.ts +150 -0
  260. package/src/plugin/context.ts +421 -0
  261. package/src/plugin/define.ts +792 -0
  262. package/src/plugin/definition-error.ts +19 -0
  263. package/src/plugin/limits.ts +70 -0
  264. package/src/plugin/load-worker.ts +50 -0
  265. package/src/plugin/load.ts +82 -0
  266. package/src/plugin/manifest.ts +561 -0
  267. package/src/plugin/refusals.ts +94 -0
  268. package/src/plugin/rest-schemas.ts +354 -0
  269. package/src/plugin/sandbox.ts +123 -0
  270. package/src/plugin/testing.ts +612 -0
  271. package/src/plugin/web-react.ts +45 -0
  272. package/src/plugin/web.ts +403 -0
  273. package/src/plugin-nav.ts +132 -0
  274. package/src/plugin-testing.ts +10 -0
  275. package/src/plugin-web-react.ts +4 -0
  276. package/src/plugin-web.ts +35 -0
  277. package/src/plugin.ts +126 -0
  278. package/src/router.ts +73 -0
  279. package/src/routes.ts +7 -0
  280. package/src/schemas.ts +115 -18
  281. package/src/styles/console.css +330 -1
  282. package/src/tokens.ts +101 -0
  283. package/src/vocabulary.ts +30 -0
  284. package/src/web.ts +2 -0
  285. package/template/plugin/.gitignore +2 -0
  286. package/template/plugin/README.md +26 -0
  287. package/template/plugin/SKILLCDN.md +13 -0
  288. package/template/plugin/build.mjs +40 -0
  289. package/template/plugin/console-plugin.json +50 -0
  290. package/template/plugin/package.json +27 -0
  291. package/template/plugin/skills/__key__/SKILL.md +23 -0
  292. package/template/plugin/src/check.test.ts +34 -0
  293. package/template/plugin/src/server/index.test.ts +41 -0
  294. package/template/plugin/src/server/index.ts +72 -0
  295. package/template/plugin/src/web/index.css +51 -0
  296. package/template/plugin/src/web/index.ts +115 -0
  297. package/template/plugin/tsconfig.json +17 -0
  298. package/skills/working-the-board/references/setting-up.md +0 -3
  299. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
package/src/console.tsx CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type RestDecisionPatch,
24
24
  type RestEvent,
25
25
  type RestMyDecisions,
26
+ type RestPluginDetail,
26
27
  type RestProject,
27
28
  type RestProjectInput,
28
29
  type RestProjectPatch,
@@ -50,6 +51,8 @@ import { MyDecisions } from "./components/my-decisions.js";
50
51
  import { PackTokenWays } from "./components/pack-token.js";
51
52
  import { PAGE_SIZE } from "./components/paged.js";
52
53
  import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
54
+ import { PluginFrame, PluginWidgets } from "./components/plugin-frame.js";
55
+ import { PluginSettingsList, PluginSettingsView } from "./components/plugin-settings.js";
53
56
  import {
54
57
  MemberList,
55
58
  type MemberListProps,
@@ -104,6 +107,7 @@ import {
104
107
  type Messages,
105
108
  useWords,
106
109
  } from "./i18n/index.js";
110
+ import { pluginNavItems, pluginPagesAt, pluginWordOf } from "./plugin-nav.js";
107
111
  import {
108
112
  boardHref,
109
113
  calendarHref,
@@ -121,6 +125,8 @@ import {
121
125
  PATHS,
122
126
  personAgentsHref,
123
127
  personWorkHref,
128
+ pluginHref,
129
+ pluginSettingsHref,
124
130
  projectHref,
125
131
  projectSettingsHref,
126
132
  type Route,
@@ -350,6 +356,14 @@ function useFollowLinks(navigation: Navigation): void {
350
356
  }
351
357
 
352
358
  /** The key of the project a route is on, or nothing. */
359
+ /** The files chosen or pasted on a page, as the client sends them: each by its name, its bytes and its type where the browser knows one. */
360
+ const uploadsOf = (files: readonly File[]): FileUpload[] =>
361
+ files.map((file) => ({
362
+ name: file.name,
363
+ bytes: file,
364
+ ...(file.type === "" ? {} : { contentType: file.type }),
365
+ }));
366
+
353
367
  const projectOf = (route: Route): string | undefined =>
354
368
  "project" in route ? route.project : undefined;
355
369
 
@@ -393,11 +407,27 @@ function ProjectsPage(props: {
393
407
  readonly data: ConsoleData;
394
408
  readonly components: ConsoleComponents;
395
409
  readonly navigation: Navigation;
410
+ readonly client: ConsoleClient;
411
+ readonly language: string;
396
412
  /** Whether the form for a new project is open: the page is at its address. */
397
413
  readonly making: boolean;
398
414
  }) {
399
415
  const words = useWords().projects;
400
- const { data, components, navigation } = props;
416
+ const { data, components, navigation, client, language } = props;
417
+ const me = data.me;
418
+ /** The plugins' widgets at a place of this page (ADR-0054), for the person's roles. */
419
+ const widgets = (place: "home:top" | "home:bottom") =>
420
+ me === undefined ? null : (
421
+ <PluginWidgets
422
+ plugins={data.plugins}
423
+ place={place}
424
+ me={me}
425
+ language={language}
426
+ client={client}
427
+ navigation={navigation}
428
+ nameOf={(name) => pluginWordOf(name, language)}
429
+ />
430
+ );
401
431
  const { busy, error, act } = useAction();
402
432
  const href = (project: RestProject) => projectHref(project.key);
403
433
  // A project is made by an administrator (ADR-0045): a member sees the list alone, the form's
@@ -415,6 +445,7 @@ function ProjectsPage(props: {
415
445
  )}
416
446
  </div>
417
447
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
448
+ {widgets("home:top")}
418
449
  {making && (
419
450
  <section className="sc-panel sc-panel-form" aria-label={words.form.aria}>
420
451
  <ProjectForm
@@ -450,10 +481,150 @@ function ProjectsPage(props: {
450
481
  />
451
482
  }
452
483
  />
484
+ {widgets("home:bottom")}
485
+ </>
486
+ );
487
+ }
488
+
489
+ /**
490
+ * A plugin's page (ADR-0054): the sub-tabs of its pages at the level, where it has several, above
491
+ * the frame of the one shown, the first when the address names none. Nothing for a plugin the
492
+ * person has no page of, or that is not on the project.
493
+ */
494
+ function PluginPage(props: {
495
+ readonly route: Extract<Route, { name: "plugin" }>;
496
+ readonly data: ConsoleData;
497
+ readonly project: RestProject | undefined;
498
+ readonly client: ConsoleClient;
499
+ readonly language: string;
500
+ readonly navigation: Navigation;
501
+ }) {
502
+ const words = useWords();
503
+ const { route, data, project, client, language, navigation } = props;
504
+ const plugin = data.plugins.find(
505
+ (candidate) => candidate.key === route.plugin && candidate.state === "enabled",
506
+ );
507
+ const bound =
508
+ route.level === "workspace" ||
509
+ data.projectPlugins.some((candidate) => candidate.key === route.plugin);
510
+ const pages = plugin === undefined ? [] : pluginPagesAt(plugin, route.level);
511
+ const page = route.page === undefined ? pages[0] : pages.find((p) => p.key === route.page);
512
+ if (plugin === undefined || !bound || page === undefined || data.me === undefined) {
513
+ return <EmptyState title={words.plugins.noPage.title} body={words.plugins.noPage.body} />;
514
+ }
515
+ const projectKey = route.level === "project" ? route.project : undefined;
516
+ return (
517
+ <>
518
+ {pages.length > 1 && (
519
+ <nav className="sc-subtabs" aria-label={pluginWordOf(plugin.name, language)}>
520
+ {pages.map((candidate) => (
521
+ <a
522
+ key={candidate.key}
523
+ href={pluginHref(plugin.key, candidate.key, projectKey)}
524
+ className={candidate.key === page.key ? "sc-tab sc-tab-current" : "sc-tab"}
525
+ aria-current={candidate.key === page.key ? "page" : undefined}
526
+ >
527
+ {pluginWordOf(candidate.name, language)}
528
+ </a>
529
+ ))}
530
+ </nav>
531
+ )}
532
+ <PluginFrame
533
+ key={`${plugin.key}:${page.key}:${projectKey ?? ""}`}
534
+ plugin={plugin}
535
+ page={page.key}
536
+ project={project}
537
+ path={route.rest}
538
+ me={data.me}
539
+ language={language}
540
+ client={client}
541
+ navigation={navigation}
542
+ events={project === undefined ? undefined : data.events}
543
+ title={words.plugins.frame(pluginWordOf(page.name, language))}
544
+ />
453
545
  </>
454
546
  );
455
547
  }
456
548
 
549
+ /**
550
+ * The plugins' settings (docs/specs/plugins.md, "The settings pages"): the list, or one plugin
551
+ * whole, read again after every change; for an administrator.
552
+ */
553
+ function PluginSettingsPage(props: {
554
+ readonly plugin: string | undefined;
555
+ readonly data: ConsoleData;
556
+ readonly client: ConsoleClient;
557
+ readonly language: string;
558
+ readonly navigation: Navigation;
559
+ }) {
560
+ const words = useWords();
561
+ const { data, client, language, navigation } = props;
562
+ const settings = words.plugins.settings;
563
+ const administrator = data.me?.person?.role === "admin";
564
+ const [detail, setDetail] = useState<RestPluginDetail | undefined>(undefined);
565
+ const [failure, setFailure] = useState<string | undefined>(undefined);
566
+ const key = props.plugin;
567
+ const read = useCallback(async () => {
568
+ if (key === undefined) {
569
+ return;
570
+ }
571
+ try {
572
+ setDetail(await client.plugin(key).get());
573
+ setFailure(undefined);
574
+ } catch (error) {
575
+ setFailure(error instanceof ApiError ? error.message : words.common.somethingWentWrong);
576
+ }
577
+ }, [client, key, words.common.somethingWentWrong]);
578
+ useEffect(() => {
579
+ setDetail(undefined);
580
+ void read();
581
+ }, [read]);
582
+ if (!administrator) {
583
+ return <NothingHere />;
584
+ }
585
+ if (key === undefined) {
586
+ return (
587
+ <>
588
+ <div className="sc-page-head">
589
+ <h1 className="sc-page-title">{settings.title}</h1>
590
+ </div>
591
+ <PluginSettingsList
592
+ plugins={data.plugins}
593
+ language={language}
594
+ href={(plugin) => pluginSettingsHref(plugin.key)}
595
+ />
596
+ </>
597
+ );
598
+ }
599
+ if (failure !== undefined) {
600
+ return <EmptyState title={words.common.somethingWentWrong} body={failure} />;
601
+ }
602
+ if (detail === undefined) {
603
+ return (
604
+ <div className="sc-loading">
605
+ <Spinner label={words.common.loading} />
606
+ </div>
607
+ );
608
+ }
609
+ return (
610
+ <PluginSettingsView
611
+ detail={detail}
612
+ projects={data.projects}
613
+ people={data.people}
614
+ language={language}
615
+ client={client}
616
+ onChanged={async () => {
617
+ await read();
618
+ await data.actions.refreshPlugins();
619
+ }}
620
+ onPurged={() => {
621
+ void data.actions.refreshPlugins();
622
+ navigation.replace(pluginSettingsHref());
623
+ }}
624
+ />
625
+ );
626
+ }
627
+
457
628
  /**
458
629
  * The archive of the board (ADR-0029): the tasks put away, newest first, a page at a time from
459
630
  * the server, each brought back from here by whoever may work in the project.
@@ -700,9 +871,9 @@ function TaskPage(props: {
700
871
  })
701
872
  }
702
873
  onMove={(target, state) => void act(() => data.actions.updateTask(target.id, { state }))}
703
- onRaiseDecision={(input) =>
874
+ onRaiseDecision={(input, files) =>
704
875
  void act(async () => {
705
- await data.actions.raiseDecision(input);
876
+ await data.actions.raiseDecision(input, uploadsOf(files));
706
877
  navigation.replace(here);
707
878
  })
708
879
  }
@@ -726,14 +897,7 @@ function TaskPage(props: {
726
897
  }
727
898
  onComment={(draft) =>
728
899
  void act(async () => {
729
- await data.actions.comment(task.id, {
730
- body: draft.body,
731
- files: draft.files.map((file) => ({
732
- name: file.name,
733
- bytes: file,
734
- ...(file.type === "" ? {} : { contentType: file.type }),
735
- })),
736
- });
900
+ await data.actions.comment(task.id, { body: draft.body, files: uploadsOf(draft.files) });
737
901
  setComments(await taskComments(task.id));
738
902
  })
739
903
  }
@@ -747,6 +911,8 @@ function ProjectPage(props: {
747
911
  readonly data: ConsoleData;
748
912
  readonly components: ConsoleComponents;
749
913
  readonly navigation: Navigation;
914
+ readonly client: ConsoleClient;
915
+ readonly language: string;
750
916
  }) {
751
917
  const words = useWords();
752
918
  const { route, project, data, components, navigation } = props;
@@ -1146,6 +1312,7 @@ function ProjectPage(props: {
1146
1312
  tasks={tasksById}
1147
1313
  taskHref={href}
1148
1314
  docHref={toDoc}
1315
+ fileHref={(fileId) => projectPath(key, "files", fileId)}
1149
1316
  decisionHref={(decision) => decisionHref(key, decision.id)}
1150
1317
  page={route.page}
1151
1318
  pageHref={(page) => decisionsHref(key, page)}
@@ -1172,9 +1339,9 @@ function ProjectPage(props: {
1172
1339
  deciders={data.members.map((member) => member.person)}
1173
1340
  me={data.me?.person ?? undefined}
1174
1341
  busy={busy}
1175
- onSubmit={(input: RestDecisionInput) =>
1342
+ onSubmit={(input: RestDecisionInput, files: readonly File[]) =>
1176
1343
  void act(async () => {
1177
- const made = await data.actions.raiseDecision(input);
1344
+ const made = await data.actions.raiseDecision(input, uploadsOf(files));
1178
1345
  navigation.replace(decisionHref(key, made.id));
1179
1346
  })
1180
1347
  }
@@ -1201,6 +1368,7 @@ function ProjectPage(props: {
1201
1368
  task={decision.taskId === null ? undefined : tasksById.get(decision.taskId)}
1202
1369
  taskHref={href}
1203
1370
  docHref={toDoc}
1371
+ fileHref={(fileId) => projectPath(key, "files", fileId)}
1204
1372
  onAnswer={onAnswer}
1205
1373
  onUpdate={onUpdate}
1206
1374
  busy={busy}
@@ -1313,6 +1481,18 @@ function ProjectPage(props: {
1313
1481
  <ProjectSettingsPage route={route} project={project} data={data} navigation={navigation} />
1314
1482
  );
1315
1483
  }
1484
+ if (route.name === "plugin") {
1485
+ return (
1486
+ <PluginPage
1487
+ route={route}
1488
+ data={data}
1489
+ project={project}
1490
+ client={props.client}
1491
+ language={props.language}
1492
+ navigation={navigation}
1493
+ />
1494
+ );
1495
+ }
1316
1496
  return <NothingHere />;
1317
1497
  }
1318
1498
 
@@ -1578,9 +1758,10 @@ function WorkspaceSettingsPage(props: {
1578
1758
  readonly form: "rules" | "workspace" | "pack-token" | undefined;
1579
1759
  readonly data: ConsoleData;
1580
1760
  readonly navigation: Navigation;
1761
+ readonly language: string;
1581
1762
  }) {
1582
1763
  const words = useWords();
1583
- const { form, data, navigation } = props;
1764
+ const { form, data, navigation, language } = props;
1584
1765
  const { busy, error, act } = useAction();
1585
1766
  const administrator = data.me?.person?.role === "admin";
1586
1767
  const workspace = data.me?.workspace;
@@ -1729,7 +1910,7 @@ function WorkspaceSettingsPage(props: {
1729
1910
  </a>
1730
1911
  )}
1731
1912
  </div>
1732
- <WorkspaceSettingsView workspace={workspace} />
1913
+ <WorkspaceSettingsView workspace={workspace} console={data.me?.console} />
1733
1914
  </section>
1734
1915
  )}
1735
1916
  <section className="sc-panel" aria-label={words.rules.title}>
@@ -1753,6 +1934,20 @@ function WorkspaceSettingsPage(props: {
1753
1934
  />
1754
1935
  )}
1755
1936
  </section>
1937
+ {administrator && (
1938
+ <section className="sc-panel" aria-label={words.plugins.settings.title}>
1939
+ <h2 className="sc-section-title">{words.plugins.settings.title}</h2>
1940
+ {data.plugins.length === 0 ? (
1941
+ <p className="sc-settings-hint">{words.plugins.settings.empty.body}</p>
1942
+ ) : (
1943
+ <PluginSettingsList
1944
+ plugins={data.plugins}
1945
+ language={language}
1946
+ href={(plugin) => pluginSettingsHref(plugin.key)}
1947
+ />
1948
+ )}
1949
+ </section>
1950
+ )}
1756
1951
  <section className="sc-panel" aria-label={words.settings.categories}>
1757
1952
  <h2 className="sc-section-title">{words.settings.categories}</h2>
1758
1953
  {data.categories.length > 0 && (
@@ -2109,6 +2304,7 @@ function MyDecisionsPage(props: { readonly data: ConsoleData }) {
2109
2304
  addressed={{ items: addressed.items, more: addressed.more, onMore: addressed.readMore }}
2110
2305
  raised={{ items: raised.items, more: raised.more, onMore: raised.readMore }}
2111
2306
  decisionHref={decisionHref}
2307
+ fileHref={(projectKey, fileId) => projectPath(projectKey, "files", fileId)}
2112
2308
  onAnswer={onAnswer}
2113
2309
  busy={busy || addressed.busy || raised.busy}
2114
2310
  />
@@ -2174,11 +2370,36 @@ function WorkspacePage(props: {
2174
2370
  readonly data: ConsoleData;
2175
2371
  readonly components: ConsoleComponents;
2176
2372
  readonly navigation: Navigation;
2373
+ readonly client: ConsoleClient;
2374
+ readonly language: string;
2177
2375
  }) {
2178
2376
  const words = useWords();
2179
- const { route, data, components, navigation } = props;
2377
+ const { route, data, components, navigation, client, language } = props;
2180
2378
  const [fresh, setFresh] = useState<RestTokenCreated | undefined>(undefined);
2181
2379
  const { busy, error, act } = useAction();
2380
+ if (route.name === "plugin" && route.level === "workspace") {
2381
+ return (
2382
+ <PluginPage
2383
+ route={route}
2384
+ data={data}
2385
+ project={undefined}
2386
+ client={client}
2387
+ language={language}
2388
+ navigation={navigation}
2389
+ />
2390
+ );
2391
+ }
2392
+ if (route.name === "plugins-settings") {
2393
+ return (
2394
+ <PluginSettingsPage
2395
+ plugin={route.plugin}
2396
+ data={data}
2397
+ client={client}
2398
+ language={language}
2399
+ navigation={navigation}
2400
+ />
2401
+ );
2402
+ }
2182
2403
 
2183
2404
  if (route.name === "work") {
2184
2405
  return <WorkPage data={data} components={components} week={route.week} />;
@@ -2190,7 +2411,14 @@ function WorkspacePage(props: {
2190
2411
  return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
2191
2412
  }
2192
2413
  if (route.name === "workspace-settings") {
2193
- return <WorkspaceSettingsPage form={route.form} data={data} navigation={navigation} />;
2414
+ return (
2415
+ <WorkspaceSettingsPage
2416
+ form={route.form}
2417
+ data={data}
2418
+ navigation={navigation}
2419
+ language={language}
2420
+ />
2421
+ );
2194
2422
  }
2195
2423
  if (route.name === "categories") {
2196
2424
  const me = data.me?.person ?? undefined;
@@ -2351,6 +2579,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2351
2579
  const languages = config.languages ?? DEFAULT_LANGUAGES;
2352
2580
  const pages = config.pages ?? [];
2353
2581
 
2582
+ // biome-ignore lint/correctness/noComponentHookFactories: createConsole runs once, when a console is built, never during a render; App is that console's one root component, closing over its config.
2354
2583
  function App(props: { readonly initialPath?: string | undefined }) {
2355
2584
  const { location, navigation } = useNavigation(props.initialPath);
2356
2585
  useFollowLinks(navigation);
@@ -2435,6 +2664,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2435
2664
  href: PATHS.decisions,
2436
2665
  label: words.nav.myDecisions,
2437
2666
  current: route.name === "my-decisions",
2667
+ count: data.me?.decisions?.waiting,
2438
2668
  },
2439
2669
  {
2440
2670
  href: PATHS.people,
@@ -2453,13 +2683,6 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2453
2683
  current: route.name === "agents" || route.name === "new-token",
2454
2684
  },
2455
2685
  ]),
2456
- // The settings are read by everyone: the rules every agent follows, the categories and
2457
- // what the colours mean, where the skills come from; kept by an administrator.
2458
- {
2459
- href: PATHS.settings,
2460
- label: words.nav.settings,
2461
- current: route.name === "workspace-settings" || route.name === "categories",
2462
- },
2463
2686
  ...pages
2464
2687
  .filter((candidate) => candidate.level === "workspace")
2465
2688
  .map((candidate) => ({
@@ -2470,6 +2693,28 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2470
2693
  route.level === "workspace" &&
2471
2694
  route.page === candidate.key,
2472
2695
  })),
2696
+ // The plugins' tabs (ADR-0054): the groups that hold a plugin the person has a page of.
2697
+ ...pluginNavItems({
2698
+ plugins: data.plugins,
2699
+ level: "workspace",
2700
+ current: {
2701
+ plugin:
2702
+ route.name === "plugin" && route.level === "workspace" ? route.plugin : undefined,
2703
+ },
2704
+ words,
2705
+ language: pack.tag,
2706
+ }),
2707
+ // The settings are read by everyone: the rules every agent follows, the categories and
2708
+ // what the colours mean, where the skills come from, the plugins; kept by an
2709
+ // administrator. Last, after the organization's own tabs and the plugins'.
2710
+ {
2711
+ href: PATHS.settings,
2712
+ label: words.nav.settings,
2713
+ current:
2714
+ route.name === "workspace-settings" ||
2715
+ route.name === "categories" ||
2716
+ route.name === "plugins-settings",
2717
+ },
2473
2718
  ];
2474
2719
  const nav: NavItem[] =
2475
2720
  projectKey === undefined
@@ -2521,12 +2766,6 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2521
2766
  label: words.nav.members,
2522
2767
  current: route.name === "members",
2523
2768
  },
2524
- // Read by every member: what the project is, and what its agents are asked (ADR-0023).
2525
- {
2526
- href: projectHref(projectKey, "settings"),
2527
- label: words.nav.settings,
2528
- current: route.name === "settings",
2529
- },
2530
2769
  ...pages
2531
2770
  .filter((candidate) => candidate.level === "project")
2532
2771
  .map((candidate) => ({
@@ -2537,6 +2776,24 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2537
2776
  route.level === "project" &&
2538
2777
  route.page === candidate.key,
2539
2778
  })),
2779
+ ...pluginNavItems({
2780
+ plugins: data.plugins,
2781
+ level: "project",
2782
+ project: { key: projectKey, plugins: data.projectPlugins },
2783
+ current: {
2784
+ plugin:
2785
+ route.name === "plugin" && route.level === "project" ? route.plugin : undefined,
2786
+ },
2787
+ words,
2788
+ language: pack.tag,
2789
+ }),
2790
+ // Read by every member: what the project is, and what its agents are asked
2791
+ // (ADR-0023). Last, after the organization's own tabs and the plugins'.
2792
+ {
2793
+ href: projectHref(projectKey, "settings"),
2794
+ label: words.nav.settings,
2795
+ current: route.name === "settings",
2796
+ },
2540
2797
  ];
2541
2798
  /** A page of the console's own at the address, or nothing there. */
2542
2799
  const customPageOf = (key: string, level: CustomPage["level"]): ReactNode => {
@@ -2562,6 +2819,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2562
2819
  data={data}
2563
2820
  components={components}
2564
2821
  navigation={navigation}
2822
+ client={client}
2823
+ language={pack.tag}
2565
2824
  making={route.name === "new-project"}
2566
2825
  />
2567
2826
  );
@@ -2574,6 +2833,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2574
2833
  data={data}
2575
2834
  components={components}
2576
2835
  navigation={navigation}
2836
+ client={client}
2837
+ language={pack.tag}
2577
2838
  />
2578
2839
  );
2579
2840
  } else if (data.projectError !== undefined) {
@@ -2600,6 +2861,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2600
2861
  data={data}
2601
2862
  components={components}
2602
2863
  navigation={navigation}
2864
+ client={client}
2865
+ language={pack.tag}
2603
2866
  />
2604
2867
  );
2605
2868
  }
@@ -2632,7 +2895,17 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2632
2895
  route.name === "calendar" ||
2633
2896
  (route.name === "goal" && route.form === undefined) ||
2634
2897
  (route.name === "custom" &&
2635
- pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
2898
+ pages.some((candidate) => candidate.key === route.page && candidate.wide === true)) ||
2899
+ (route.name === "plugin" &&
2900
+ data.plugins.some(
2901
+ (candidate) =>
2902
+ candidate.key === route.plugin &&
2903
+ pluginPagesAt(candidate, route.level).some(
2904
+ (candidate, index) =>
2905
+ candidate.wide &&
2906
+ (route.page === undefined ? index === 0 : candidate.key === route.page),
2907
+ ),
2908
+ ))
2636
2909
  }
2637
2910
  onNavigate={(href) => navigation.go(href)}
2638
2911
  onSignOut={viaToken ? undefined : () => void data.actions.signOut()}