@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/client.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  connectPath,
6
6
  personTokensPath,
7
7
  personWorkPath,
8
+ pluginPath,
8
9
  projectPath,
9
10
  REST_ROUTES,
10
11
  restPath,
@@ -52,6 +53,21 @@ import {
52
53
  type RestPerson,
53
54
  type RestPersonPatch,
54
55
  type RestPersonWork,
56
+ type RestPluginCommandInput,
57
+ type RestPluginDetail,
58
+ type RestPluginEnableInput,
59
+ type RestPluginEvents,
60
+ type RestPluginFileHandle,
61
+ type RestPluginGrant,
62
+ type RestPluginGrantInput,
63
+ type RestPluginGrants,
64
+ type RestPluginInvocation,
65
+ type RestPluginInvokeInput,
66
+ type RestPluginRecordPage,
67
+ type RestPluginRecordRead,
68
+ type RestPluginRecordVersions,
69
+ type RestPluginSettingsPatch,
70
+ type RestPlugins,
55
71
  type RestProject,
56
72
  type RestProjectInput,
57
73
  type RestProjectPatch,
@@ -108,6 +124,16 @@ import {
108
124
  restPeopleSchema,
109
125
  restPersonSchema,
110
126
  restPersonWorkSchema,
127
+ restPluginDetailSchema,
128
+ restPluginEventsSchema,
129
+ restPluginFileHandleSchema,
130
+ restPluginGrantSchema,
131
+ restPluginGrantsSchema,
132
+ restPluginInvocationSchema,
133
+ restPluginRecordPageSchema,
134
+ restPluginRecordReadSchema,
135
+ restPluginRecordVersionsSchema,
136
+ restPluginsSchema,
111
137
  restProjectRulesSchema,
112
138
  restProjectSchema,
113
139
  restProjectsSchema,
@@ -299,7 +325,8 @@ export interface ProjectClient {
299
325
  * while the decision waits, for as long as it allows, and answers the decision as it stands.
300
326
  */
301
327
  awaitDecision(id: string, waitSeconds: number, signal?: AbortSignal): Promise<RestDecision>;
302
- raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
328
+ /** Raises a decision; with `files`, the files put on it (ADR-0055) go in the one request. */
329
+ raiseDecision(input: RestDecisionInput, files?: readonly FileUpload[]): Promise<RestDecision>;
303
330
  answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
304
331
  /** Grows a decision's record: its context, or what followed. */
305
332
  updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
@@ -317,7 +344,7 @@ export interface ProjectClient {
317
344
  handIn(runId: string, input: RestArtifactInput): Promise<RestRun>;
318
345
  /** Hands in a file, on a run of the asker's: the console keeps the bytes. */
319
346
  handInFile(runId: string, input: FileUpload): Promise<RestRun>;
320
- /** Where the bytes of a file of the project are read, by its id: handed in by a run, put in a comment, or attached to a page. */
347
+ /** Where the bytes of a file of the project are read, by its id: handed in by a run, put in a comment or on a decision, or attached to a page. */
321
348
  fileUrl(fileId: string): string;
322
349
  /** The bytes of a file of the project, by its id, with the name and the type the console says. */
323
350
  readFile(fileId: string, signal?: AbortSignal): Promise<FileRead>;
@@ -404,6 +431,126 @@ export interface ProjectClient {
404
431
  updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
405
432
  }
406
433
 
434
+ /** What a read of a plugin's collection is narrowed by (docs/specs/plugins.md, "Records"). */
435
+ export interface RecordFilter {
436
+ /** Conditions on indexed fields, as `ctx.records().list` takes them. */
437
+ readonly where?: Readonly<Record<string, unknown>> | undefined;
438
+ /** A field, with `-` before it for descending. */
439
+ readonly order?: string | undefined;
440
+ readonly limit?: number | undefined;
441
+ /** The cursor the last page answered with. */
442
+ readonly after?: string | undefined;
443
+ /** Words to search the collection's search fields for. */
444
+ readonly q?: string | undefined;
445
+ /** The key of a project, for the records linked to it. */
446
+ readonly project?: string | undefined;
447
+ readonly archived?: boolean | undefined;
448
+ }
449
+
450
+ /**
451
+ * The client of one plugin (docs/specs/plugins.md, "REST"): its commands and queries run as the
452
+ * person the client acts as, its records read within their roles, its skills, its events; and,
453
+ * for an administrator, enabling, configuring and granting. Every answer is in the console's
454
+ * generic shapes; what a command or a query itself answers is the plugin's own.
455
+ */
456
+ export interface PluginClient {
457
+ readonly key: string;
458
+ /** The plugin whole: its commands and queries with their arguments, its skills, its settings. */
459
+ get(signal?: AbortSignal): Promise<RestPluginDetail>;
460
+ /** Runs a command or a query by the words as typed, which the console parses against the definition. */
461
+ invoke(input: RestPluginInvokeInput): Promise<RestPluginInvocation>;
462
+ /** Runs a command with its arguments as JSON, by name, for a page. */
463
+ command(name: string, input: RestPluginCommandInput): Promise<RestPluginInvocation>;
464
+ /** Runs a query with its arguments as JSON; the answer is the query's own JSON. */
465
+ query(
466
+ name: string,
467
+ args: Readonly<Record<string, unknown>>,
468
+ options?: { readonly project?: string | undefined },
469
+ signal?: AbortSignal,
470
+ ): Promise<unknown>;
471
+ /** Where a query's answer is read as bytes, for one that answers a file. */
472
+ queryUrl(
473
+ name: string,
474
+ args: Readonly<Record<string, unknown>>,
475
+ options?: { readonly project?: string | undefined },
476
+ ): string;
477
+ /** Uploads a file for a command, with the `files` capability: the handle a `file` argument takes, good for an hour. */
478
+ upload(input: FileUpload): Promise<RestPluginFileHandle>;
479
+ /** A page of a collection's records, as the person may read them. */
480
+ records(
481
+ collection: string,
482
+ filter?: RecordFilter,
483
+ signal?: AbortSignal,
484
+ ): Promise<RestPluginRecordPage>;
485
+ /** One record, or one version of it. */
486
+ record(
487
+ collection: string,
488
+ id: string,
489
+ version?: number,
490
+ signal?: AbortSignal,
491
+ ): Promise<RestPluginRecordRead>;
492
+ recordVersions(
493
+ collection: string,
494
+ id: string,
495
+ signal?: AbortSignal,
496
+ ): Promise<RestPluginRecordVersions>;
497
+ /** Where a record's file is read, by its id. */
498
+ fileUrl(fileId: string): string;
499
+ /** The bytes of a record's file, with the name and the type the console says. */
500
+ readFile(fileId: string, signal?: AbortSignal): Promise<FileRead>;
501
+ /** Where the frame of a page or a widget is (ADR-0054), on a project when its key is given. */
502
+ frameUrl(options: {
503
+ readonly page?: string | undefined;
504
+ readonly widget?: string | undefined;
505
+ readonly project?: string | undefined;
506
+ }): string;
507
+ /** The events the plugin emitted after number `after`, oldest first: what a frame hears. */
508
+ pluginEvents(
509
+ page: { readonly after: number; readonly limit: number },
510
+ signal?: AbortSignal,
511
+ ): Promise<RestPluginEvents>;
512
+ /** Removes everything the console keeps of a disabled plugin, the key named again; for an administrator. */
513
+ purge(confirm: string): Promise<void>;
514
+ /** The plugin's skills, a pack of its own read from its package, as a project's five reads. */
515
+ skills(signal?: AbortSignal): Promise<RestSkills>;
516
+ skill(path: string, cursor?: string, signal?: AbortSignal): Promise<RestSkillLoad>;
517
+ skillFile(
518
+ path: string,
519
+ page?: { readonly offset?: number | undefined; readonly limit?: number | undefined },
520
+ signal?: AbortSignal,
521
+ ): Promise<RestSkillFile>;
522
+ skillBrowse(
523
+ page?: {
524
+ readonly path?: string | undefined;
525
+ readonly cursor?: string | undefined;
526
+ readonly limit?: number | undefined;
527
+ },
528
+ signal?: AbortSignal,
529
+ ): Promise<RestSkillBrowse>;
530
+ skillFind(
531
+ query: string,
532
+ options?: {
533
+ readonly path?: string | undefined;
534
+ readonly cursor?: string | undefined;
535
+ readonly limit?: number | undefined;
536
+ },
537
+ signal?: AbortSignal,
538
+ ): Promise<RestSkillFind>;
539
+ /** The plugin's events, newest first, a page at a time. */
540
+ events(page?: Page, signal?: AbortSignal): Promise<RestEvents>;
541
+ /** Enables the plugin, with consent to every capability it asks; for an administrator. */
542
+ enable(input: RestPluginEnableInput): Promise<RestPluginDetail>;
543
+ disable(): Promise<RestPluginDetail>;
544
+ /** Changes the settings' values, without secrets; `null` clears one. */
545
+ updateSettings(patch: RestPluginSettingsPatch): Promise<RestPluginDetail>;
546
+ /** Seals a secret for the plugin; it is answered by nothing from then on. */
547
+ setSecret(name: string, value: string): Promise<void>;
548
+ removeSecret(name: string): Promise<void>;
549
+ grants(signal?: AbortSignal): Promise<RestPluginGrants>;
550
+ grant(input: RestPluginGrantInput): Promise<RestPluginGrant>;
551
+ revoke(grantId: string): Promise<RestPluginGrant>;
552
+ }
553
+
407
554
  export interface ConsoleClient {
408
555
  me(signal?: AbortSignal): Promise<RestMe>;
409
556
  /** Gives up the token this client acts with, so that it is good for nothing from now on: what `console logout` does. For a token; a session has none. */
@@ -443,6 +590,10 @@ export interface ConsoleClient {
443
590
  createProject(input: RestProjectInput): Promise<RestProject>;
444
591
  /** The board of one project. */
445
592
  project(key: string): ProjectClient;
593
+ /** The plugins the asker may know of: the enabled ones, every one for an administrator. */
594
+ plugins(signal?: AbortSignal): Promise<RestPlugins>;
595
+ /** One plugin, by its key. */
596
+ plugin(key: string): PluginClient;
446
597
  /** The workspace's own events, the ones about no project, after event number `after`. */
447
598
  events(after: number, signal?: AbortSignal): Promise<RestEvents>;
448
599
  /** Where a picture from elsewhere is read through the console (ADR-0048), given the https URL that names it. */
@@ -624,11 +775,9 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
624
775
  return form;
625
776
  };
626
777
 
627
- /** A comment with files as the parts of a form: the part `body`, and a part `file` for each file. */
628
- const commentFormOf = (input: CommentInput): FormData => {
629
- const form = new FormData();
630
- form.set("body", input.body);
631
- for (const file of input.files ?? []) {
778
+ /** The files appended to a form, a part `file` each, with its name and its type. */
779
+ const appendFiles = (form: FormData, files: readonly FileUpload[]): void => {
780
+ for (const file of files) {
632
781
  const bytes =
633
782
  file.bytes instanceof Blob ? file.bytes : new Blob([Uint8Array.from(file.bytes)]);
634
783
  form.append(
@@ -637,6 +786,21 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
637
786
  file.name,
638
787
  );
639
788
  }
789
+ };
790
+
791
+ /** A comment with files as the parts of a form: the part `body`, and a part `file` for each file. */
792
+ const commentFormOf = (input: CommentInput): FormData => {
793
+ const form = new FormData();
794
+ form.set("body", input.body);
795
+ appendFiles(form, input.files ?? []);
796
+ return form;
797
+ };
798
+
799
+ /** A decision with files as the parts of a form (ADR-0055): the part `decision`, what the JSON would say, and a part `file` for each file. */
800
+ const decisionFormOf = (input: RestDecisionInput, files: readonly FileUpload[]): FormData => {
801
+ const form = new FormData();
802
+ form.set("decision", JSON.stringify(input));
803
+ appendFiles(form, files);
640
804
  return form;
641
805
  };
642
806
 
@@ -710,8 +874,13 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
710
874
  restDecisionSchema,
711
875
  signal,
712
876
  ),
713
- raiseDecision: (input) =>
714
- request("POST", `${base}${at("decisions")}`, input, restDecisionSchema),
877
+ raiseDecision: (input, files = []) =>
878
+ request(
879
+ "POST",
880
+ `${base}${at("decisions")}`,
881
+ files.length > 0 ? decisionFormOf(input, files) : input,
882
+ restDecisionSchema,
883
+ ),
715
884
  answerDecision: (id, input) =>
716
885
  request("POST", `${base}${at("decisions", id)}/answer`, input, restDecisionSchema),
717
886
  updateDecision: (id, patch) =>
@@ -915,6 +1084,147 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
915
1084
  };
916
1085
  };
917
1086
 
1087
+ /** A schema that takes whatever JSON came: a query's own answer. */
1088
+ const anything: Schema<unknown> = { safeParse: (data) => ({ success: true, data }) };
1089
+
1090
+ const pluginClient = (key: string): PluginClient => {
1091
+ const at = (...parts: readonly string[]) => pluginPath(key, ...parts);
1092
+ const skillsQuery = (part: string, params: Record<string, string | undefined>): string =>
1093
+ withQuery(at("skills", part), params);
1094
+ const queryUrl = (
1095
+ name: string,
1096
+ args: Readonly<Record<string, unknown>>,
1097
+ options: { readonly project?: string | undefined } = {},
1098
+ ): string =>
1099
+ withQuery(at("queries", name), { args: JSON.stringify(args), project: options.project });
1100
+ return {
1101
+ key,
1102
+ get: (signal) => request("GET", `${base}${at()}`, undefined, restPluginDetailSchema, signal),
1103
+ invoke: (input) =>
1104
+ request("POST", `${base}${at("invoke")}`, input, restPluginInvocationSchema),
1105
+ command: (name, input) =>
1106
+ request("POST", `${base}${at("commands", name)}`, input, restPluginInvocationSchema),
1107
+ query: (name, args, options = {}, signal) =>
1108
+ request("GET", queryUrl(name, args, options), undefined, anything, signal),
1109
+ queryUrl,
1110
+ upload: (input) =>
1111
+ request("POST", `${base}${at("files")}`, formOf(input), restPluginFileHandleSchema),
1112
+ records: (collection, filter = {}, signal) =>
1113
+ request(
1114
+ "GET",
1115
+ withQuery(at("records", collection), {
1116
+ where: filter.where === undefined ? undefined : JSON.stringify(filter.where),
1117
+ order: filter.order,
1118
+ limit: number(filter.limit),
1119
+ after: filter.after,
1120
+ q: filter.q,
1121
+ project: filter.project,
1122
+ archived: flag(filter.archived),
1123
+ }),
1124
+ undefined,
1125
+ restPluginRecordPageSchema,
1126
+ signal,
1127
+ ),
1128
+ record: (collection, id, version, signal) =>
1129
+ request(
1130
+ "GET",
1131
+ withQuery(at("records", collection, id), { version: number(version) }),
1132
+ undefined,
1133
+ restPluginRecordReadSchema,
1134
+ signal,
1135
+ ),
1136
+ recordVersions: (collection, id, signal) =>
1137
+ request(
1138
+ "GET",
1139
+ `${base}${at("records", collection, id, "versions")}`,
1140
+ undefined,
1141
+ restPluginRecordVersionsSchema,
1142
+ signal,
1143
+ ),
1144
+ fileUrl: (fileId) => `${base}${at("files", fileId)}`,
1145
+ readFile: (fileId, signal) => read(`${base}${at("files", fileId)}`, fileId, signal),
1146
+ frameUrl: (options) =>
1147
+ withQuery(at("frame"), {
1148
+ page: options.page,
1149
+ widget: options.widget,
1150
+ project: options.project,
1151
+ }),
1152
+ pluginEvents: (page, signal) =>
1153
+ request(
1154
+ "GET",
1155
+ withQuery(at("plugin-events"), { after: String(page.after), limit: String(page.limit) }),
1156
+ undefined,
1157
+ restPluginEventsSchema,
1158
+ signal,
1159
+ ),
1160
+ purge: (confirm) => request("POST", `${base}${at("purge")}`, { confirm }, undefined),
1161
+ skills: (signal) =>
1162
+ request("GET", `${base}${at("skills")}`, undefined, restSkillsSchema, signal),
1163
+ skill: (path, cursor, signal) =>
1164
+ request(
1165
+ "GET",
1166
+ skillsQuery("one", { path, cursor }),
1167
+ undefined,
1168
+ restSkillLoadSchema,
1169
+ signal,
1170
+ ),
1171
+ skillFile: (path, page = {}, signal) =>
1172
+ request(
1173
+ "GET",
1174
+ skillsQuery("file", { path, offset: number(page.offset), limit: number(page.limit) }),
1175
+ undefined,
1176
+ restSkillFileSchema,
1177
+ signal,
1178
+ ),
1179
+ skillBrowse: (page = {}, signal) =>
1180
+ request(
1181
+ "GET",
1182
+ skillsQuery("browse", {
1183
+ path: page.path,
1184
+ cursor: page.cursor,
1185
+ limit: number(page.limit),
1186
+ }),
1187
+ undefined,
1188
+ restSkillBrowseSchema,
1189
+ signal,
1190
+ ),
1191
+ skillFind: (query, options = {}, signal) =>
1192
+ request(
1193
+ "GET",
1194
+ skillsQuery("find", {
1195
+ query,
1196
+ path: options.path,
1197
+ cursor: options.cursor,
1198
+ limit: number(options.limit),
1199
+ }),
1200
+ undefined,
1201
+ restSkillFindSchema,
1202
+ signal,
1203
+ ),
1204
+ events: (page = {}, signal) =>
1205
+ request(
1206
+ "GET",
1207
+ withQuery(at("events"), { limit: number(page.limit), offset: number(page.offset) }),
1208
+ undefined,
1209
+ restEventsSchema,
1210
+ signal,
1211
+ ),
1212
+ enable: (input) => request("POST", `${base}${at("enable")}`, input, restPluginDetailSchema),
1213
+ disable: () => request("POST", `${base}${at("disable")}`, undefined, restPluginDetailSchema),
1214
+ updateSettings: (patch) =>
1215
+ request("PATCH", `${base}${at("settings")}`, patch, restPluginDetailSchema),
1216
+ setSecret: (name, value) =>
1217
+ request("PUT", `${base}${at("secrets", name)}`, { value }, undefined),
1218
+ removeSecret: (name) =>
1219
+ request("DELETE", `${base}${at("secrets", name)}`, undefined, undefined),
1220
+ grants: (signal) =>
1221
+ request("GET", `${base}${at("grants")}`, undefined, restPluginGrantsSchema, signal),
1222
+ grant: (input) => request("POST", `${base}${at("grants")}`, input, restPluginGrantSchema),
1223
+ revoke: (grantId) =>
1224
+ request("DELETE", `${base}${at("grants", grantId)}`, undefined, restPluginGrantSchema),
1225
+ };
1226
+ };
1227
+
918
1228
  return {
919
1229
  me: (signal) => request("GET", `${base}${REST_ROUTES.me}`, undefined, restMeSchema, signal),
920
1230
  giveUpToken: () => request("DELETE", `${base}${REST_ROUTES.meToken}`, undefined, undefined),
@@ -972,6 +1282,9 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
972
1282
  createProject: (input) =>
973
1283
  request("POST", `${base}${REST_ROUTES.projects}`, input, restProjectSchema),
974
1284
  project: projectClient,
1285
+ plugins: (signal) =>
1286
+ request("GET", `${base}${REST_ROUTES.plugins}`, undefined, restPluginsSchema, signal),
1287
+ plugin: pluginClient,
975
1288
  events: (after, signal) =>
976
1289
  request(
977
1290
  "GET",
package/src/compat.ts CHANGED
@@ -113,7 +113,7 @@ export interface ClientPolicy {
113
113
  * last changed what the command reads or sends (ADR-0038), whose changelog entry begins with
114
114
  * `Breaking:`; `pnpm check` holds it to a version the package has, or is about to have.
115
115
  */
116
- export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.1" };
116
+ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.2" };
117
117
 
118
118
  /**
119
119
  * The oldest console this command works with (ADR-0038): raised by the release that last
@@ -122,7 +122,7 @@ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.1" };
122
122
  * itself, naming the command to install instead; `pnpm check` holds it as it holds the
123
123
  * console's floor.
124
124
  */
125
- export const CONSOLE_AT_LEAST = "0.4.1";
125
+ export const CONSOLE_AT_LEAST = "0.4.2";
126
126
 
127
127
  /** `common` from `atLeast` on, where that floor is within it; `common` as it is otherwise. */
128
128
  function narrowed(common: VersionRange, atLeast: string | undefined): VersionRange {
@@ -80,7 +80,28 @@ const HOLD_MS = 300;
80
80
  const HOLD_SLACK = 8;
81
81
  /** How near the pointer comes to the board's edge before the board scrolls to show more columns. */
82
82
  const EDGE = 48;
83
- const EDGE_STEP = 12;
83
+ /** How fast the board scrolls, in pixels a second, with the pointer at the edge itself; slower further in. */
84
+ const EDGE_SPEED = 720;
85
+ /** The most a frame counts for, so that a tab that was away does not leap when it is back. */
86
+ const FRAME_AT_MOST_MS = 64;
87
+
88
+ /**
89
+ * How far the board scrolls in one frame for a pointer at `x`, the board spanning `left` to
90
+ * `right` on the screen: nothing away from the edges; towards the edge the pointer is near, the
91
+ * nearer the faster, and no faster beyond it. Asked every frame while a card is held, not only
92
+ * when the pointer moves, so that a card held still at the edge carries the board on, as an icon
93
+ * held at the edge of a phone's screen turns the page. Negative is leftwards.
94
+ */
95
+ export function edgeScrollOf(x: number, left: number, right: number, elapsedMs: number): number {
96
+ const ms = Math.min(Math.max(elapsedMs, 0), FRAME_AT_MOST_MS);
97
+ const depth = (distance: number) => Math.min(1, Math.max(0, (EDGE - distance) / EDGE));
98
+ const leftward = depth(x - left);
99
+ const rightward = depth(right - x);
100
+ if (leftward === 0 && rightward === 0) {
101
+ return 0;
102
+ }
103
+ return ((leftward > rightward ? -leftward : rightward) * EDGE_SPEED * ms) / 1000;
104
+ }
84
105
 
85
106
  /** The state a column stands for, read off its `data-column`; nothing for a value that is not one. */
86
107
  export function columnStateOf(value: string | null | undefined): TaskState | undefined {
@@ -422,10 +443,10 @@ export function Board(props: BoardProps) {
422
443
  }, []);
423
444
 
424
445
  // While a card is held, the pointer is followed over the whole document: the column under it
425
- // is marked, the board scrolls when the pointer nears its edge, and the release moves the task.
426
- // A finger's hold is timed here, and once the card has come loose the page no longer scrolls
427
- // under the finger, nor asks for the menu a long press would otherwise bring up; the click that
428
- // follows a drag opens nothing.
446
+ // is marked, the board scrolls while the pointer is near its edge, moving or still, and the
447
+ // release moves the task. A finger's hold is timed here, and once the card has come loose the
448
+ // page no longer scrolls under the finger, nor asks for the menu a long press would otherwise
449
+ // bring up; the click that follows a drag opens nothing.
429
450
  const held = drag !== null;
430
451
  useEffect(() => {
431
452
  if (!held) {
@@ -435,8 +456,42 @@ export function Board(props: BoardProps) {
435
456
  columnStateOf(
436
457
  document.elementFromPoint(x, y)?.closest<HTMLElement>("[data-column]")?.dataset.column,
437
458
  );
459
+ // Where the pointer last was, for the frames between its moves.
460
+ const pointer = { x: current.current?.x ?? 0, y: current.current?.y ?? 0 };
461
+ // The board is carried on every frame the loose card is near its edge; what a frame's share
462
+ // comes to short of a whole pixel is kept for the next, since a scroll position holds whole
463
+ // ones, and the column under the pointer is marked again where the board moved under it.
464
+ let frame = 0;
465
+ let at: number | undefined;
466
+ let rest = 0;
467
+ const carry = (now: number) => {
468
+ frame = window.requestAnimationFrame(carry);
469
+ const elapsed = at === undefined ? 0 : now - at;
470
+ at = now;
471
+ const element = board.current;
472
+ if (element === null || !loose.current) {
473
+ return;
474
+ }
475
+ const rect = element.getBoundingClientRect();
476
+ const share = edgeScrollOf(pointer.x, rect.left, rect.right, elapsed) + rest;
477
+ const whole = Math.trunc(share);
478
+ rest = share - whole;
479
+ if (whole === 0) {
480
+ return;
481
+ }
482
+ const was = element.scrollLeft;
483
+ element.scrollLeft = was + whole;
484
+ if (element.scrollLeft !== was) {
485
+ const over = columnUnder(pointer.x, pointer.y);
486
+ setDrag((before) =>
487
+ before === null || before.over === over ? before : { ...before, over },
488
+ );
489
+ }
490
+ };
438
491
  const follow = (event: PointerEvent) => {
439
492
  const { clientX: x, clientY: y } = event;
493
+ pointer.x = x;
494
+ pointer.y = y;
440
495
  setDrag((before) => {
441
496
  if (before === null) {
442
497
  return before;
@@ -455,15 +510,6 @@ export function Board(props: BoardProps) {
455
510
  }
456
511
  return { ...before, x, y, moving: true, over: columnUnder(x, y) };
457
512
  });
458
- const element = board.current;
459
- if (element !== null && loose.current) {
460
- const rect = element.getBoundingClientRect();
461
- if (x < rect.left + EDGE) {
462
- element.scrollLeft -= EDGE_STEP;
463
- } else if (x > rect.right - EDGE) {
464
- element.scrollLeft += EDGE_STEP;
465
- }
466
- }
467
513
  };
468
514
  const loosen = () => {
469
515
  setDrag((before) => {
@@ -502,6 +548,7 @@ export function Board(props: BoardProps) {
502
548
  };
503
549
  const quiet = (event: Event) => event.preventDefault();
504
550
  const timer = current.current?.hold === true ? window.setTimeout(loosen, HOLD_MS) : undefined;
551
+ frame = window.requestAnimationFrame(carry);
505
552
  window.addEventListener("pointermove", follow);
506
553
  window.addEventListener("pointerup", release);
507
554
  window.addEventListener("pointercancel", release);
@@ -511,6 +558,7 @@ export function Board(props: BoardProps) {
511
558
  if (timer !== undefined) {
512
559
  window.clearTimeout(timer);
513
560
  }
561
+ window.cancelAnimationFrame(frame);
514
562
  window.removeEventListener("pointermove", follow);
515
563
  window.removeEventListener("pointerup", release);
516
564
  window.removeEventListener("pointercancel", release);
@@ -305,6 +305,7 @@ export function TaskThread(props: TaskThreadProps) {
305
305
  decision={item.decision}
306
306
  task={props.task}
307
307
  docHref={props.docHref}
308
+ fileHref={fileHref}
308
309
  onAnswer={props.onAnswer}
309
310
  onUpdate={props.onUpdateDecision}
310
311
  busy={props.busy}