@skillcdn/console 0.4.3 → 0.4.4

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 (231) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +3 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +3 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +95 -20
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +4 -2
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +32 -9
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +17 -14
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +5 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +23 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts.map +1 -1
  21. package/dist/cli/plugin-cli.js +10 -1
  22. package/dist/cli/plugin-cli.js.map +1 -1
  23. package/dist/cli/plugin-init.d.ts +2 -0
  24. package/dist/cli/plugin-init.d.ts.map +1 -1
  25. package/dist/cli/plugin-init.js +6 -0
  26. package/dist/cli/plugin-init.js.map +1 -1
  27. package/dist/client.d.ts +4 -0
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +2 -0
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.js +1 -1
  32. package/dist/components/board.d.ts +8 -0
  33. package/dist/components/board.d.ts.map +1 -1
  34. package/dist/components/board.js +3 -3
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -0
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +23 -5
  39. package/dist/components/decision-form.js.map +1 -1
  40. package/dist/components/decision-list.d.ts +5 -1
  41. package/dist/components/decision-list.d.ts.map +1 -1
  42. package/dist/components/decision-list.js +25 -5
  43. package/dist/components/decision-list.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +7 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/goals.d.ts.map +1 -1
  48. package/dist/components/goals.js +2 -2
  49. package/dist/components/goals.js.map +1 -1
  50. package/dist/components/home.d.ts +35 -0
  51. package/dist/components/home.d.ts.map +1 -0
  52. package/dist/components/home.js +326 -0
  53. package/dist/components/home.js.map +1 -0
  54. package/dist/components/pack-connect.d.ts.map +1 -1
  55. package/dist/components/pack-connect.js +5 -4
  56. package/dist/components/pack-connect.js.map +1 -1
  57. package/dist/components/people.d.ts +2 -0
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +15 -2
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +3 -12
  62. package/dist/components/plugin-frame.d.ts.map +1 -1
  63. package/dist/components/plugin-frame.js +53 -20
  64. package/dist/components/plugin-frame.js.map +1 -1
  65. package/dist/components/plugin-settings.d.ts.map +1 -1
  66. package/dist/components/plugin-settings.js +151 -26
  67. package/dist/components/plugin-settings.js.map +1 -1
  68. package/dist/components/projects.d.ts +0 -8
  69. package/dist/components/projects.d.ts.map +1 -1
  70. package/dist/components/projects.js +1 -13
  71. package/dist/components/projects.js.map +1 -1
  72. package/dist/components/task-view.d.ts.map +1 -1
  73. package/dist/components/task-view.js +2 -1
  74. package/dist/components/task-view.js.map +1 -1
  75. package/dist/components/ui.d.ts.map +1 -1
  76. package/dist/components/ui.js +5 -2
  77. package/dist/components/ui.js.map +1 -1
  78. package/dist/connect-page.d.ts.map +1 -1
  79. package/dist/connect-page.js +5 -6
  80. package/dist/connect-page.js.map +1 -1
  81. package/dist/console.css +761 -191
  82. package/dist/console.d.ts +10 -3
  83. package/dist/console.d.ts.map +1 -1
  84. package/dist/console.js +33 -13
  85. package/dist/console.js.map +1 -1
  86. package/dist/data.d.ts +4 -2
  87. package/dist/data.d.ts.map +1 -1
  88. package/dist/data.js +5 -0
  89. package/dist/data.js.map +1 -1
  90. package/dist/docs-page.d.ts.map +1 -1
  91. package/dist/docs-page.js +6 -5
  92. package/dist/docs-page.js.map +1 -1
  93. package/dist/home-tiles.d.ts +92 -0
  94. package/dist/home-tiles.d.ts.map +1 -0
  95. package/dist/home-tiles.js +294 -0
  96. package/dist/home-tiles.js.map +1 -0
  97. package/dist/i18n/en.d.ts +174 -0
  98. package/dist/i18n/en.d.ts.map +1 -1
  99. package/dist/i18n/en.js +175 -1
  100. package/dist/i18n/en.js.map +1 -1
  101. package/dist/i18n/index.d.ts +4 -2
  102. package/dist/i18n/index.d.ts.map +1 -1
  103. package/dist/i18n/index.js +10 -11
  104. package/dist/i18n/index.js.map +1 -1
  105. package/dist/i18n/ko.d.ts.map +1 -1
  106. package/dist/i18n/ko.js +170 -3
  107. package/dist/i18n/ko.js.map +1 -1
  108. package/dist/index.d.ts +4 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +4 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/limits.d.ts +6 -0
  113. package/dist/limits.d.ts.map +1 -1
  114. package/dist/limits.js +6 -0
  115. package/dist/limits.js.map +1 -1
  116. package/dist/plugin/bridge.d.ts +44 -3
  117. package/dist/plugin/bridge.d.ts.map +1 -1
  118. package/dist/plugin/bridge.js +26 -0
  119. package/dist/plugin/bridge.js.map +1 -1
  120. package/dist/plugin/capabilities.d.ts +7 -1
  121. package/dist/plugin/capabilities.d.ts.map +1 -1
  122. package/dist/plugin/capabilities.js +14 -1
  123. package/dist/plugin/capabilities.js.map +1 -1
  124. package/dist/plugin/context.d.ts +18 -1
  125. package/dist/plugin/context.d.ts.map +1 -1
  126. package/dist/plugin/define.d.ts +1 -0
  127. package/dist/plugin/define.d.ts.map +1 -1
  128. package/dist/plugin/define.js +1 -1
  129. package/dist/plugin/define.js.map +1 -1
  130. package/dist/plugin/limits.d.ts +5 -2
  131. package/dist/plugin/limits.d.ts.map +1 -1
  132. package/dist/plugin/limits.js +5 -2
  133. package/dist/plugin/limits.js.map +1 -1
  134. package/dist/plugin/manifest.d.ts +83 -6
  135. package/dist/plugin/manifest.d.ts.map +1 -1
  136. package/dist/plugin/manifest.js +111 -7
  137. package/dist/plugin/manifest.js.map +1 -1
  138. package/dist/plugin/rest-schemas.d.ts +76 -0
  139. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  140. package/dist/plugin/rest-schemas.js +23 -2
  141. package/dist/plugin/rest-schemas.js.map +1 -1
  142. package/dist/plugin/sandbox.d.ts +4 -3
  143. package/dist/plugin/sandbox.d.ts.map +1 -1
  144. package/dist/plugin/sandbox.js +33 -6
  145. package/dist/plugin/sandbox.js.map +1 -1
  146. package/dist/plugin/semver.d.ts +26 -0
  147. package/dist/plugin/semver.d.ts.map +1 -0
  148. package/dist/plugin/semver.js +76 -0
  149. package/dist/plugin/semver.js.map +1 -0
  150. package/dist/plugin/testing.d.ts.map +1 -1
  151. package/dist/plugin/testing.js +2 -1
  152. package/dist/plugin/testing.js.map +1 -1
  153. package/dist/plugin/web.d.ts +10 -1
  154. package/dist/plugin/web.d.ts.map +1 -1
  155. package/dist/plugin/web.js +34 -0
  156. package/dist/plugin/web.js.map +1 -1
  157. package/dist/plugin.d.ts +3 -2
  158. package/dist/plugin.d.ts.map +1 -1
  159. package/dist/plugin.js +3 -2
  160. package/dist/plugin.js.map +1 -1
  161. package/dist/rules.d.ts +2 -2
  162. package/dist/rules.js +3 -3
  163. package/dist/schemas.d.ts +497 -2
  164. package/dist/schemas.d.ts.map +1 -1
  165. package/dist/schemas.js +96 -9
  166. package/dist/schemas.js.map +1 -1
  167. package/dist/use-action.d.ts +8 -2
  168. package/dist/use-action.d.ts.map +1 -1
  169. package/dist/use-action.js +13 -6
  170. package/dist/use-action.js.map +1 -1
  171. package/dist/vocabulary.d.ts +3 -1
  172. package/dist/vocabulary.d.ts.map +1 -1
  173. package/dist/vocabulary.js +3 -0
  174. package/dist/vocabulary.js.map +1 -1
  175. package/docs/specs/cli.md +4 -3
  176. package/docs/specs/plugins.md +46 -33
  177. package/docs/specs/rest.md +16 -15
  178. package/package.json +1 -1
  179. package/skills/building-a-plugin/SKILL.md +8 -7
  180. package/skills/planning-the-board/references/setting-up.md +2 -1
  181. package/skills/working-the-board/SKILL.md +3 -3
  182. package/src/api.ts +6 -0
  183. package/src/cli/cli.ts +124 -19
  184. package/src/cli/format.ts +42 -9
  185. package/src/cli/help.ts +17 -13
  186. package/src/cli/plugin-check.ts +41 -2
  187. package/src/cli/plugin-cli.ts +10 -1
  188. package/src/cli/plugin-init.ts +8 -0
  189. package/src/client.ts +8 -0
  190. package/src/compat.ts +1 -1
  191. package/src/components/board.tsx +3 -3
  192. package/src/components/decision-form.tsx +75 -19
  193. package/src/components/decision-list.tsx +45 -13
  194. package/src/components/event-feed.tsx +7 -1
  195. package/src/components/goals.tsx +4 -1
  196. package/src/components/home.tsx +615 -0
  197. package/src/components/pack-connect.tsx +5 -4
  198. package/src/components/people.tsx +136 -54
  199. package/src/components/plugin-frame.tsx +62 -63
  200. package/src/components/plugin-settings.tsx +428 -87
  201. package/src/components/projects.tsx +0 -55
  202. package/src/components/task-view.tsx +7 -0
  203. package/src/components/ui.tsx +6 -2
  204. package/src/connect-page.tsx +5 -6
  205. package/src/console.tsx +80 -30
  206. package/src/data.ts +9 -2
  207. package/src/docs-page.tsx +6 -5
  208. package/src/home-tiles.ts +438 -0
  209. package/src/i18n/en.ts +178 -0
  210. package/src/i18n/index.ts +17 -14
  211. package/src/i18n/ko.ts +174 -3
  212. package/src/index.ts +7 -5
  213. package/src/limits.ts +6 -0
  214. package/src/plugin/bridge.ts +77 -1
  215. package/src/plugin/capabilities.ts +17 -1
  216. package/src/plugin/context.ts +18 -1
  217. package/src/plugin/define.ts +4 -1
  218. package/src/plugin/limits.ts +5 -2
  219. package/src/plugin/manifest.ts +154 -10
  220. package/src/plugin/rest-schemas.ts +27 -1
  221. package/src/plugin/sandbox.ts +36 -8
  222. package/src/plugin/semver.ts +89 -0
  223. package/src/plugin/testing.ts +2 -1
  224. package/src/plugin/web.ts +49 -0
  225. package/src/plugin.ts +18 -0
  226. package/src/rules.ts +3 -3
  227. package/src/schemas.ts +118 -10
  228. package/src/styles/console.css +761 -191
  229. package/src/use-action.ts +16 -7
  230. package/src/vocabulary.ts +4 -0
  231. package/template/plugin/console-plugin.json +3 -1
package/src/client.ts CHANGED
@@ -498,6 +498,8 @@ export interface PluginClient {
498
498
  fileUrl(fileId: string): string;
499
499
  /** The bytes of a record's file, with the name and the type the console says. */
500
500
  readFile(fileId: string, signal?: AbortSignal): Promise<FileRead>;
501
+ /** A person's picture, for a frame, which names the person and never an address (ADR-0063). */
502
+ personPicture(personId: string, signal?: AbortSignal): Promise<FileRead>;
501
503
  /** Where the frame of a page or a widget is (ADR-0054), on a project when its key is given. */
502
504
  frameUrl(options: {
503
505
  readonly page?: string | undefined;
@@ -543,6 +545,8 @@ export interface PluginClient {
543
545
  disable(): Promise<RestPluginDetail>;
544
546
  /** Changes the settings' values, without secrets; `null` clears one. */
545
547
  updateSettings(patch: RestPluginSettingsPatch): Promise<RestPluginDetail>;
548
+ /** Says what an enabled plugin holds: those it requires, named again, and the optional ones to turn on. */
549
+ updateConsent(consent: readonly string[]): Promise<RestPluginDetail>;
546
550
  /** Seals a secret for the plugin; it is answered by nothing from then on. */
547
551
  setSecret(name: string, value: string): Promise<void>;
548
552
  removeSecret(name: string): Promise<void>;
@@ -1143,6 +1147,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1143
1147
  ),
1144
1148
  fileUrl: (fileId) => `${base}${at("files", fileId)}`,
1145
1149
  readFile: (fileId, signal) => read(`${base}${at("files", fileId)}`, fileId, signal),
1150
+ personPicture: (personId, signal) =>
1151
+ read(`${base}${at("people", personId, "picture")}`, personId, signal),
1146
1152
  frameUrl: (options) =>
1147
1153
  withQuery(at("frame"), {
1148
1154
  page: options.page,
@@ -1213,6 +1219,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1213
1219
  disable: () => request("POST", `${base}${at("disable")}`, undefined, restPluginDetailSchema),
1214
1220
  updateSettings: (patch) =>
1215
1221
  request("PATCH", `${base}${at("settings")}`, patch, restPluginDetailSchema),
1222
+ updateConsent: (consent) =>
1223
+ request("PATCH", `${base}${at("consent")}`, { consent }, restPluginDetailSchema),
1216
1224
  setSecret: (name, value) =>
1217
1225
  request("PUT", `${base}${at("secrets", name)}`, { value }, undefined),
1218
1226
  removeSecret: (name) =>
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.2" };
116
+ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.4" };
117
117
 
118
118
  /**
119
119
  * The oldest console this command works with (ADR-0038): raised by the release that last
@@ -71,13 +71,13 @@ export interface BoardProps {
71
71
  export const FOLDED_STATES: readonly TaskState[] = ["done", "dropped"];
72
72
 
73
73
  /** How far a mouse travels on a card before the press is a drag and not a click. */
74
- const DRAG_SLACK = 4;
74
+ export const DRAG_SLACK = 4;
75
75
  /**
76
76
  * How long a finger rests on a card before the card comes loose, and how far it may wander
77
77
  * meanwhile; a finger that moves sooner is scrolling the page, not moving the card.
78
78
  */
79
- const HOLD_MS = 300;
80
- const HOLD_SLACK = 8;
79
+ export const HOLD_MS = 300;
80
+ export 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
83
  /** How fast the board scrolls, in pixels a second, with the pointer at the edge itself; slower further in. */
@@ -1,6 +1,7 @@
1
1
  import { type ClipboardEvent, type FormEvent, useMemo, useState } from "react";
2
2
  import {
3
3
  MAX_BODY_LENGTH,
4
+ MAX_DECIDERS,
4
5
  MAX_FILES_PER_DECISION,
5
6
  MAX_OPTION_LABEL_LENGTH,
6
7
  MAX_OPTIONS,
@@ -39,6 +40,23 @@ export interface DecisionFormProps {
39
40
  readonly onCancel?: (() => void) | undefined;
40
41
  }
41
42
 
43
+ /**
44
+ * What the form sends of whom the decision is for: nothing for the raiser alone, `deciderId` for
45
+ * one other person, `deciderIds` for several (ADR-0057), the first among them repeated as
46
+ * `deciderId` for a console of before.
47
+ */
48
+ export function decidersInput(
49
+ deciders: readonly RestPerson[],
50
+ me: RestPerson | undefined,
51
+ ): Pick<RestDecisionInput, "deciderId" | "deciderIds"> {
52
+ const ids = deciders.map((person) => person.id);
53
+ if (ids.length === 0 || (ids.length === 1 && ids[0] === me?.id)) {
54
+ return {};
55
+ }
56
+ const first = ids[0] ?? "";
57
+ return ids.length === 1 ? { deciderId: first } : { deciderId: first, deciderIds: ids };
58
+ }
59
+
42
60
  /** The people of the project found by the words typed, a page at a time, as a picker searches. */
43
61
  export function peopleSearch(people: readonly RestPerson[]): PickerSearch<RestPerson> {
44
62
  return async (query, page) => {
@@ -63,9 +81,20 @@ export function DecisionForm(props: DecisionFormProps) {
63
81
  const [body, setBody] = useState("");
64
82
  const [about, setAbout] = useState<RestTask | undefined>(undefined);
65
83
  const [options, setOptions] = useState<readonly string[]>(["", ""]);
66
- const [decider, setDecider] = useState<RestPerson | undefined>(props.me);
84
+ // Whom it is for (ADR-0042, ADR-0057): the raiser, until others are named in their place.
85
+ const [deciders, setDeciders] = useState<readonly RestPerson[]>(
86
+ props.me === undefined ? [] : [props.me],
87
+ );
67
88
  const [files, setFiles] = useState<readonly File[]>([]);
68
- const findDeciders = useMemo(() => peopleSearch(props.deciders ?? []), [props.deciders]);
89
+ const findDeciders = useMemo(
90
+ () =>
91
+ peopleSearch(
92
+ (props.deciders ?? []).filter(
93
+ (person) => !deciders.some((chosen) => chosen.id === person.id),
94
+ ),
95
+ ),
96
+ [props.deciders, deciders],
97
+ );
69
98
 
70
99
  const labels = options.map((option) => option.trim()).filter((option) => option.length > 0);
71
100
  const ready = question.trim().length > 0 && labels.length >= MIN_OPTIONS;
@@ -115,7 +144,7 @@ export function DecisionForm(props: DecisionFormProps) {
115
144
  body,
116
145
  options: labels.map((label) => ({ label })),
117
146
  taskId: props.taskId ?? about?.id ?? null,
118
- ...(decider === undefined || decider.id === props.me?.id ? {} : { deciderId: decider.id }),
147
+ ...decidersInput(deciders, props.me),
119
148
  },
120
149
  files,
121
150
  );
@@ -195,24 +224,51 @@ export function DecisionForm(props: DecisionFormProps) {
195
224
  </div>
196
225
  )}
197
226
  {props.deciders !== undefined && (
198
- <div className="sc-field">
199
- <span className="sc-field-label">{form.decider}</span>
200
- <Picker
201
- value={decider}
202
- search={findDeciders}
203
- keyOf={(person) => person.id}
204
- render={(person) => (
205
- <>
206
- <PersonChip person={person} size="sm" />
227
+ <fieldset className="sc-field sc-workers-field">
228
+ <legend className="sc-field-label">{form.decider}</legend>
229
+ <div className="sc-chips">
230
+ {deciders.map((person) => (
231
+ <span key={person.id} className="sc-chip">
232
+ <PersonChip person={person} />
207
233
  {person.id === props.me?.id && <span className="sc-muted"> {form.me}</span>}
208
- </>
234
+ <button
235
+ type="button"
236
+ className="sc-chip-remove"
237
+ aria-label={`${words.common.remove}: ${personName(person)}`}
238
+ disabled={props.busy}
239
+ onClick={() =>
240
+ setDeciders((current) => current.filter((chosen) => chosen.id !== person.id))
241
+ }
242
+ >
243
+ <span aria-hidden="true">&times;</span>
244
+ </button>
245
+ </span>
246
+ ))}
247
+ {deciders.length === 0 && <span className="sc-muted">{form.deciderPlaceholder}</span>}
248
+ {deciders.length < MAX_DECIDERS && (
249
+ <Picker
250
+ value={undefined}
251
+ search={findDeciders}
252
+ keyOf={(person) => person.id}
253
+ render={(person) => (
254
+ <>
255
+ <PersonChip person={person} size="sm" />
256
+ {person.id === props.me?.id && <span className="sc-muted"> {form.me}</span>}
257
+ </>
258
+ )}
259
+ label={form.addDecider}
260
+ placeholder={form.addDecider}
261
+ disabled={props.busy}
262
+ onChange={(person) => {
263
+ if (person !== undefined) {
264
+ setDeciders((current) => [...current, person]);
265
+ }
266
+ }}
267
+ />
209
268
  )}
210
- label={form.decider}
211
- placeholder={form.deciderPlaceholder}
212
- disabled={props.busy}
213
- onChange={(person) => setDecider(person ?? props.me)}
214
- />
215
- </div>
269
+ </div>
270
+ {deciders.length > 1 && <p className="sc-field-hint">{form.decidersHint}</p>}
271
+ </fieldset>
216
272
  )}
217
273
  <fieldset className="sc-field sc-options">
218
274
  <legend className="sc-field-label">{form.options}</legend>
@@ -8,6 +8,7 @@ import {
8
8
  type RestDecision,
9
9
  type RestDecisionFile,
10
10
  type RestDecisionPatch,
11
+ type RestPerson,
11
12
  type RestTask,
12
13
  } from "../api.js";
13
14
  import { useWords } from "../i18n/index.js";
@@ -43,13 +44,34 @@ export interface DecisionCardProps {
43
44
  readonly busy?: boolean | undefined;
44
45
  }
45
46
 
47
+ /** The word beside who answered: through their agent, when the answer came that way (ADR-0056). */
48
+ function answeredWord(words: ReturnType<typeof useWords>, agent: string | null | undefined) {
49
+ return agent === null || agent === undefined
50
+ ? words.decisions.answeredBy
51
+ : words.decisions.answeredThrough(agentLabel(agent));
52
+ }
53
+
54
+ /** Everyone a decision is addressed to (ADR-0057): `deciders` where the console answers it, else the one decider. */
55
+ export function decidersOf(decision: RestDecision): readonly RestPerson[] {
56
+ return decision.deciders !== undefined && decision.deciders.length > 0
57
+ ? decision.deciders
58
+ : [decision.decider];
59
+ }
60
+
61
+ /** The names of everyone a decision is addressed to, one after another. */
62
+ export function deciderNames(decision: RestDecision): string {
63
+ return decidersOf(decision).map(personName).join(", ");
64
+ }
65
+
46
66
  /**
47
67
  * Who asked, and when: the person, through their agent when a run asked; and whom the decision
48
- * is for, where that is someone else (ADR-0042).
68
+ * is for, where that is not the asker alone (ADR-0042, ADR-0057).
49
69
  */
50
70
  function Asked(props: { readonly decision: RestDecision; readonly day?: boolean }) {
51
71
  const words = useWords();
52
72
  const { decision } = props;
73
+ const deciders = decidersOf(decision);
74
+ const addressed = deciders.some((person) => person.id !== decision.raisedBy.id);
53
75
  return (
54
76
  <>
55
77
  <PersonChip person={decision.raisedBy} />{" "}
@@ -57,18 +79,17 @@ function Asked(props: { readonly decision: RestDecision; readonly day?: boolean
57
79
  ? words.decisions.asked
58
80
  : words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
59
81
  <Time iso={decision.createdAt} precision={props.day === true ? "day" : "minute"} />
60
- {decision.decider.id !== decision.raisedBy.id && (
82
+ {addressed && (
61
83
  <span className="sc-decision-for">
62
84
  <span className="sc-visually-hidden">
63
- {words.decisions.peopleLabel(
64
- personName(decision.raisedBy),
65
- personName(decision.decider),
66
- )}
85
+ {words.decisions.peopleLabel(personName(decision.raisedBy), deciderNames(decision))}
67
86
  </span>
68
87
  <span className="sc-decision-arrow" aria-hidden="true">
69
88
  →
70
89
  </span>{" "}
71
- <PersonChip person={decision.decider} />
90
+ {deciders.map((person) => (
91
+ <PersonChip key={person.id} person={person} />
92
+ ))}
72
93
  </span>
73
94
  )}
74
95
  </>
@@ -233,7 +254,20 @@ function Outcome(props: {
233
254
  {decision.outcome === null ? (
234
255
  <p className="sc-muted">{words.decisions.nothingWritten}</p>
235
256
  ) : (
236
- <Markdown source={decision.outcome} docHref={props.docHref} />
257
+ <>
258
+ <Markdown source={decision.outcome} docHref={props.docHref} />
259
+ {decision.outcomeBy != null && decision.outcomeAt != null && (
260
+ <p className="sc-decision-meta">
261
+ <PersonChip person={decision.outcomeBy} />{" "}
262
+ {decision.outcomeAgent == null
263
+ ? words.decisions.wroteWhatFollowed
264
+ : words.decisions.wroteWhatFollowedThrough(
265
+ agentLabel(decision.outcomeAgent),
266
+ )}{" "}
267
+ <Time iso={decision.outcomeAt} />
268
+ </p>
269
+ )}
270
+ </>
237
271
  )}
238
272
  {props.onUpdate !== undefined && (
239
273
  <Button
@@ -368,14 +402,12 @@ export function DecisionCard(props: DecisionCardProps) {
368
402
  </div>
369
403
  )}
370
404
  <p className="sc-decision-meta">
371
- <PersonChip person={decision.answer.by} /> {words.decisions.answeredBy}{" "}
405
+ <PersonChip person={decision.answer.by} /> {answeredWord(words, decision.answer.agent)}{" "}
372
406
  <Time iso={decision.answer.at} />
373
407
  </p>
374
408
  </div>
375
409
  ) : props.onAnswer === undefined ? (
376
- <p className="sc-decision-waiting">
377
- {words.decisions.waitingFor(personName(decision.decider))}
378
- </p>
410
+ <p className="sc-decision-waiting">{words.decisions.waitingFor(deciderNames(decision))}</p>
379
411
  ) : (
380
412
  <AnswerForm decision={decision} onAnswer={props.onAnswer} busy={props.busy} />
381
413
  )}
@@ -474,7 +506,7 @@ function RecordRow(props: {
474
506
  </div>
475
507
  )}
476
508
  <p className="sc-decision-record-meta">
477
- <PersonChip person={answer.by} /> {words.decisions.answeredBy}{" "}
509
+ <PersonChip person={answer.by} /> {answeredWord(words, answer.agent)}{" "}
478
510
  <Time iso={answer.at} precision="day" />
479
511
  </p>
480
512
  </div>
@@ -63,6 +63,10 @@ function sentenceOf(event: RestEvent, words: Messages): string {
63
63
  return feed.event.personDeactivated(login);
64
64
  case "person.reactivated":
65
65
  return feed.event.personReactivated(login);
66
+ case "person.manager_changed":
67
+ return data.manager === null || data.manager === undefined
68
+ ? feed.event.managerCleared(login)
69
+ : feed.event.managerChanged(login, data.manager);
66
70
  case "workspace.updated":
67
71
  return feed.event.workspaceUpdated(fields);
68
72
  case "project.created":
@@ -136,7 +140,9 @@ function sentenceOf(event: RestEvent, words: Messages): string {
136
140
  case "plugin.disabled":
137
141
  return feed.event.pluginDisabled(plugin);
138
142
  case "plugin.configured":
139
- return feed.event.pluginConfigured(fields, plugin);
143
+ return data.capabilities === undefined
144
+ ? feed.event.pluginConfigured(fields, plugin)
145
+ : feed.event.pluginConsented(plugin);
140
146
  case "plugin.granted":
141
147
  return feed.event.pluginGranted(data.role ?? feed.role.other, grantee, plugin);
142
148
  case "plugin.revoked":
@@ -1,5 +1,6 @@
1
1
  import { type FormEvent, type ReactNode, useState } from "react";
2
2
  import {
3
+ agentLabel,
3
4
  GOAL_STATES,
4
5
  type GoalState,
5
6
  MAX_BODY_LENGTH,
@@ -355,7 +356,9 @@ export function GoalView(props: GoalViewProps) {
355
356
  <div>
356
357
  <dt>{roadmap.setBy}</dt>
357
358
  <dd>
358
- <PersonChip person={goal.createdBy} /> <Time iso={goal.createdAt} />
359
+ <PersonChip person={goal.createdBy} />{" "}
360
+ {goal.agent != null && <>{roadmap.setThrough(agentLabel(goal.agent))} </>}
361
+ <Time iso={goal.createdAt} />
359
362
  </dd>
360
363
  </div>
361
364
  {props.editHref !== undefined && (