@skillcdn/console 0.1.14 → 0.1.15

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 (160) hide show
  1. package/README.md +5 -4
  2. package/SKILLCDN.md +2 -0
  3. package/dist/agent-name.d.ts +3 -0
  4. package/dist/agent-name.d.ts.map +1 -0
  5. package/dist/agent-name.js +9 -0
  6. package/dist/agent-name.js.map +1 -0
  7. package/dist/api.d.ts +6 -4
  8. package/dist/api.d.ts.map +1 -1
  9. package/dist/api.js +6 -4
  10. package/dist/api.js.map +1 -1
  11. package/dist/cli/cli.d.ts +2 -0
  12. package/dist/cli/cli.d.ts.map +1 -1
  13. package/dist/cli/cli.js +215 -22
  14. package/dist/cli/cli.js.map +1 -1
  15. package/dist/cli/format.d.ts +14 -4
  16. package/dist/cli/format.d.ts.map +1 -1
  17. package/dist/cli/format.js +59 -10
  18. package/dist/cli/format.js.map +1 -1
  19. package/dist/cli/help.d.ts.map +1 -1
  20. package/dist/cli/help.js +27 -11
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/main.js +5 -0
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/client.d.ts +20 -2
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +11 -2
  27. package/dist/client.js.map +1 -1
  28. package/dist/components/calendar.d.ts.map +1 -1
  29. package/dist/components/calendar.js +39 -29
  30. package/dist/components/calendar.js.map +1 -1
  31. package/dist/components/connect.d.ts +4 -1
  32. package/dist/components/connect.d.ts.map +1 -1
  33. package/dist/components/connect.js +7 -3
  34. package/dist/components/connect.js.map +1 -1
  35. package/dist/components/decision-list.d.ts.map +1 -1
  36. package/dist/components/decision-list.js +2 -2
  37. package/dist/components/decision-list.js.map +1 -1
  38. package/dist/components/documents.d.ts +5 -0
  39. package/dist/components/documents.d.ts.map +1 -1
  40. package/dist/components/documents.js +3 -2
  41. package/dist/components/documents.js.map +1 -1
  42. package/dist/components/event-feed.d.ts +1 -1
  43. package/dist/components/event-feed.d.ts.map +1 -1
  44. package/dist/components/event-feed.js +10 -1
  45. package/dist/components/event-feed.js.map +1 -1
  46. package/dist/components/people.d.ts +24 -1
  47. package/dist/components/people.d.ts.map +1 -1
  48. package/dist/components/people.js +35 -3
  49. package/dist/components/people.js.map +1 -1
  50. package/dist/components/projects.d.ts +6 -0
  51. package/dist/components/projects.d.ts.map +1 -1
  52. package/dist/components/projects.js +3 -3
  53. package/dist/components/projects.js.map +1 -1
  54. package/dist/components/rules.d.ts +40 -0
  55. package/dist/components/rules.d.ts.map +1 -0
  56. package/dist/components/rules.js +46 -0
  57. package/dist/components/rules.js.map +1 -0
  58. package/dist/components/runs.d.ts +1 -1
  59. package/dist/components/runs.d.ts.map +1 -1
  60. package/dist/components/runs.js +2 -1
  61. package/dist/components/runs.js.map +1 -1
  62. package/dist/components/skills.d.ts.map +1 -1
  63. package/dist/components/skills.js +5 -1
  64. package/dist/components/skills.js.map +1 -1
  65. package/dist/components/tokens.d.ts.map +1 -1
  66. package/dist/components/tokens.js +2 -9
  67. package/dist/components/tokens.js.map +1 -1
  68. package/dist/components/ui.d.ts +7 -2
  69. package/dist/components/ui.d.ts.map +1 -1
  70. package/dist/components/ui.js +13 -0
  71. package/dist/components/ui.js.map +1 -1
  72. package/dist/components/work.d.ts +5 -1
  73. package/dist/components/work.d.ts.map +1 -1
  74. package/dist/components/work.js +1 -1
  75. package/dist/components/work.js.map +1 -1
  76. package/dist/console.css +219 -13
  77. package/dist/console.d.ts.map +1 -1
  78. package/dist/console.js +197 -43
  79. package/dist/console.js.map +1 -1
  80. package/dist/data.d.ts +19 -2
  81. package/dist/data.d.ts.map +1 -1
  82. package/dist/data.js +27 -1
  83. package/dist/data.js.map +1 -1
  84. package/dist/i18n/en.d.ts +83 -9
  85. package/dist/i18n/en.d.ts.map +1 -1
  86. package/dist/i18n/en.js +81 -11
  87. package/dist/i18n/en.js.map +1 -1
  88. package/dist/i18n/ko.d.ts.map +1 -1
  89. package/dist/i18n/ko.js +81 -11
  90. package/dist/i18n/ko.js.map +1 -1
  91. package/dist/index.d.ts +4 -3
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +4 -3
  94. package/dist/index.js.map +1 -1
  95. package/dist/limits.d.ts +4 -0
  96. package/dist/limits.d.ts.map +1 -1
  97. package/dist/limits.js +4 -0
  98. package/dist/limits.js.map +1 -1
  99. package/dist/router.d.ts +30 -2
  100. package/dist/router.d.ts.map +1 -1
  101. package/dist/router.js +50 -10
  102. package/dist/router.js.map +1 -1
  103. package/dist/routes.d.ts +14 -4
  104. package/dist/routes.d.ts.map +1 -1
  105. package/dist/routes.js +17 -2
  106. package/dist/routes.js.map +1 -1
  107. package/dist/rules.d.ts +28 -0
  108. package/dist/rules.d.ts.map +1 -0
  109. package/dist/rules.js +102 -0
  110. package/dist/rules.js.map +1 -0
  111. package/dist/schemas.d.ts +652 -4
  112. package/dist/schemas.d.ts.map +1 -1
  113. package/dist/schemas.js +96 -6
  114. package/dist/schemas.js.map +1 -1
  115. package/dist/vocabulary.d.ts +21 -1
  116. package/dist/vocabulary.d.ts.map +1 -1
  117. package/dist/vocabulary.js +21 -0
  118. package/dist/vocabulary.js.map +1 -1
  119. package/docs/specs/cli.md +13 -9
  120. package/docs/specs/rest.md +32 -8
  121. package/package.json +1 -1
  122. package/skills/building-a-console/SKILL.md +1 -1
  123. package/skills/deploying-a-console/SKILL.md +5 -5
  124. package/skills/deploying-a-console/assets/secrets.example.json +2 -1
  125. package/skills/deploying-a-console/references/access.md +10 -6
  126. package/skills/deploying-a-console/references/resources.md +10 -7
  127. package/skills/working-the-board/SKILL.md +10 -9
  128. package/skills/working-the-board/references/project-rules.md +36 -0
  129. package/src/agent-name.ts +9 -0
  130. package/src/api.ts +37 -0
  131. package/src/cli/cli.ts +253 -24
  132. package/src/cli/format.ts +74 -10
  133. package/src/cli/help.ts +30 -10
  134. package/src/cli/main.ts +5 -0
  135. package/src/client.ts +47 -1
  136. package/src/components/calendar.tsx +63 -50
  137. package/src/components/connect.tsx +12 -6
  138. package/src/components/decision-list.tsx +2 -1
  139. package/src/components/documents.tsx +7 -3
  140. package/src/components/event-feed.tsx +16 -3
  141. package/src/components/people.tsx +244 -35
  142. package/src/components/projects.tsx +31 -8
  143. package/src/components/rules.tsx +167 -0
  144. package/src/components/runs.tsx +4 -2
  145. package/src/components/skills.tsx +22 -10
  146. package/src/components/tokens.tsx +2 -14
  147. package/src/components/ui.tsx +25 -2
  148. package/src/components/work.tsx +34 -28
  149. package/src/console.tsx +523 -45
  150. package/src/data.ts +52 -2
  151. package/src/i18n/en.ts +91 -12
  152. package/src/i18n/ko.ts +87 -12
  153. package/src/index.ts +16 -1
  154. package/src/limits.ts +4 -0
  155. package/src/router.ts +67 -12
  156. package/src/routes.ts +23 -3
  157. package/src/rules.ts +109 -0
  158. package/src/schemas.ts +121 -4
  159. package/src/styles/console.css +219 -13
  160. package/src/vocabulary.ts +30 -0
package/src/client.ts CHANGED
@@ -4,9 +4,11 @@ import {
4
4
  categoryPath,
5
5
  connectPath,
6
6
  personTokensPath,
7
+ personWorkPath,
7
8
  projectPath,
8
9
  REST_ROUTES,
9
10
  restPath,
11
+ rulesPath,
10
12
  } from "./routes.js";
11
13
  import {
12
14
  type RestAnswerInput,
@@ -31,6 +33,8 @@ import {
31
33
  type RestGoalInput,
32
34
  type RestGoalPatch,
33
35
  type RestGoals,
36
+ type RestListing,
37
+ type RestListingInput,
34
38
  type RestMe,
35
39
  type RestMember,
36
40
  type RestMemberInput,
@@ -40,11 +44,15 @@ import {
40
44
  type RestPeople,
41
45
  type RestPerson,
42
46
  type RestPersonPatch,
47
+ type RestPersonWork,
43
48
  type RestProject,
44
49
  type RestProjectInput,
45
50
  type RestProjectPatch,
51
+ type RestProjectRules,
46
52
  type RestProjects,
47
53
  type RestReportInput,
54
+ type RestRules,
55
+ type RestRulesInput,
48
56
  type RestRun,
49
57
  type RestRunEndInput,
50
58
  type RestRunInput,
@@ -73,13 +81,17 @@ import {
73
81
  restEventsSchema,
74
82
  restGoalSchema,
75
83
  restGoalsSchema,
84
+ restListingSchema,
76
85
  restMemberSchema,
77
86
  restMembersSchema,
78
87
  restMeSchema,
79
88
  restPeopleSchema,
80
89
  restPersonSchema,
90
+ restPersonWorkSchema,
91
+ restProjectRulesSchema,
81
92
  restProjectSchema,
82
93
  restProjectsSchema,
94
+ restRulesSchema,
83
95
  restRunSchema,
84
96
  restRunsSchema,
85
97
  restSkillsSchema,
@@ -243,6 +255,8 @@ export interface ProjectClient {
243
255
  eventStreamUrl(after: number): string;
244
256
  /** The project's skills, as SkillCDN serves them at its address, or the organization's. */
245
257
  skills(signal?: AbortSignal): Promise<RestSkills>;
258
+ /** The rules that hold in the project (ADR-0023): the workspace's, and what the project adds. */
259
+ rules(signal?: AbortSignal): Promise<RestProjectRules>;
246
260
  /** A folder's pages and folders, or the pages a search finds. */
247
261
  documents(filter?: DocumentFilter, signal?: AbortSignal): Promise<RestDocuments>;
248
262
  /** One document, by its path: its latest version, its links both ways, its files. */
@@ -271,11 +285,17 @@ export interface ProjectClient {
271
285
 
272
286
  export interface ConsoleClient {
273
287
  me(signal?: AbortSignal): Promise<RestMe>;
288
+ /** 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. */
289
+ giveUpToken(): Promise<void>;
274
290
  /** Keeps the person's choice of language, or none; for a person signed in. */
275
291
  updateMe(patch: RestMePatch): Promise<RestMe>;
276
292
  people(signal?: AbortSignal): Promise<RestPeople>;
277
- /** Changes what a person is; for an administrator. */
293
+ /** Changes what a person is, or whether they are in; for an administrator. */
278
294
  updatePerson(id: string, patch: RestPersonPatch): Promise<RestPerson>;
295
+ /** Lets a login in (ADR-0022): whoever signs in with that account becomes a person; for an administrator. */
296
+ createListing(input: RestListingInput): Promise<RestListing>;
297
+ /** Takes a listing away before anyone signed in with it; for an administrator. */
298
+ removeListing(id: string): Promise<void>;
279
299
  /** The projects the asker may see, with what they are in each. */
280
300
  projects(signal?: AbortSignal): Promise<RestProjects>;
281
301
  /** Makes a project, whose owner the asker becomes. */
@@ -286,6 +306,14 @@ export interface ConsoleClient {
286
306
  events(after: number, signal?: AbortSignal): Promise<RestEvents>;
287
307
  /** The categories tasks are sorted into (ADR-0019), by name. */
288
308
  categories(signal?: AbortSignal): Promise<RestCategories>;
309
+ /** The workspace's rules (ADR-0023): the console's own until an administrator writes the organization's. */
310
+ rules(signal?: AbortSignal): Promise<RestRules>;
311
+ /** Writes the organization's rules; for an administrator. The console's own, sent as they are, write nothing. */
312
+ writeRules(input: RestRulesInput): Promise<RestRules>;
313
+ /** Puts the organization's rules away, so that the console's own hold again; for an administrator. */
314
+ archiveRules(): Promise<RestRules>;
315
+ /** The versions of the organization's rules, newest first; none while the console's own hold. */
316
+ rulesVersions(signal?: AbortSignal): Promise<RestVersions>;
289
317
  /** Makes a category; for an administrator signed in. */
290
318
  createCategory(input: RestCategoryInput): Promise<RestCategory>;
291
319
  updateCategory(key: string, patch: RestCategoryPatch): Promise<RestCategory>;
@@ -293,6 +321,8 @@ export interface ConsoleClient {
293
321
  removeCategory(key: string): Promise<void>;
294
322
  /** What is on the asker's plate across the projects they may see: their open tasks, and the decisions waiting. */
295
323
  work(signal?: AbortSignal): Promise<RestWork>;
324
+ /** Another person's plate, within what the asker may see: for an administrator, or an owner of a project they are in. */
325
+ personWork(personId: string, signal?: AbortSignal): Promise<RestPersonWork>;
296
326
  /** The tokens of whoever asks. */
297
327
  tokens(signal?: AbortSignal): Promise<RestTokens>;
298
328
  /** The tokens of another person: for an administrator. */
@@ -520,6 +550,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
520
550
  eventStreamUrl: (after) => withQuery(`${at("events")}/stream`, { after: String(after) }),
521
551
  skills: (signal) =>
522
552
  request("GET", `${base}${at("skills")}`, undefined, restSkillsSchema, signal),
553
+ rules: (signal) =>
554
+ request("GET", `${base}${at("rules")}`, undefined, restProjectRulesSchema, signal),
523
555
  documents: (filter = {}, signal) =>
524
556
  request(
525
557
  "GET",
@@ -572,10 +604,15 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
572
604
 
573
605
  return {
574
606
  me: (signal) => request("GET", `${base}${REST_ROUTES.me}`, undefined, restMeSchema, signal),
607
+ giveUpToken: () => request("DELETE", `${base}${REST_ROUTES.meToken}`, undefined, undefined),
575
608
  people: (signal) =>
576
609
  request("GET", `${base}${REST_ROUTES.people}`, undefined, restPeopleSchema, signal),
577
610
  updatePerson: (id, patch) =>
578
611
  request("PATCH", `${base}${restPath("people", id)}`, patch, restPersonSchema),
612
+ createListing: (input) =>
613
+ request("POST", `${base}${REST_ROUTES.listings}`, input, restListingSchema),
614
+ removeListing: (id) =>
615
+ request("DELETE", `${base}${restPath("listings", id)}`, undefined, undefined),
579
616
  projects: (signal) =>
580
617
  request("GET", `${base}${REST_ROUTES.projects}`, undefined, restProjectsSchema, signal),
581
618
  createProject: (input) =>
@@ -591,6 +628,13 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
591
628
  ),
592
629
  categories: (signal) =>
593
630
  request("GET", `${base}${REST_ROUTES.categories}`, undefined, restCategoriesSchema, signal),
631
+ rules: (signal) =>
632
+ request("GET", `${base}${REST_ROUTES.rules}`, undefined, restRulesSchema, signal),
633
+ writeRules: (input) => request("PUT", `${base}${REST_ROUTES.rules}`, input, restRulesSchema),
634
+ archiveRules: () =>
635
+ request("POST", `${base}${rulesPath("archive")}`, undefined, restRulesSchema),
636
+ rulesVersions: (signal) =>
637
+ request("GET", `${base}${rulesPath("versions")}`, undefined, restVersionsSchema, signal),
594
638
  createCategory: (input) =>
595
639
  request("POST", `${base}${REST_ROUTES.categories}`, input, restCategorySchema),
596
640
  updateCategory: (key, patch) =>
@@ -598,6 +642,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
598
642
  removeCategory: (key) => request("DELETE", `${base}${categoryPath(key)}`, undefined, undefined),
599
643
  work: (signal) =>
600
644
  request("GET", `${base}${REST_ROUTES.work}`, undefined, restWorkSchema, signal),
645
+ personWork: (personId, signal) =>
646
+ request("GET", `${base}${personWorkPath(personId)}`, undefined, restPersonWorkSchema, signal),
601
647
  tokens: (signal) =>
602
648
  request("GET", `${base}${REST_ROUTES.tokens}`, undefined, restTokensSchema, signal),
603
649
  createToken: (input) =>
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import type { ReactNode, SyntheticEvent } from "react";
2
2
  import { OPEN_TASK_STATES, type RestCategory, type RestPerson, type RestTask } from "../api.js";
3
3
  import {
4
4
  addDays,
@@ -16,14 +16,15 @@ import {
16
16
  weekOf,
17
17
  } from "../dates.js";
18
18
  import { useLanguage, useWords } from "../i18n/index.js";
19
- import { cx, PersonChip, personName } from "./ui.js";
19
+ import { Avatar, cx, PersonChip, personName } from "./ui.js";
20
20
 
21
21
  // The calendar of a project (ADR-0019): the month as a grid, or one week on its own. A task
22
22
  // planned from a start to a due day lies across its days as a bar, in a lane of its own within
23
- // each week so that the bars of one week never cross; a task with a due day alone sits on it as
24
- // a dot, and one with a start alone is marked on that day. Who has what says whether the work
25
- // is on its days. Takes its data as props and nothing from the network; the month or the week
26
- // shown is the address's (ADR-0010).
23
+ // each week so that the bars of one week never cross; a task with one day alone, a due day or
24
+ // a start, sits on that day as a dot. On a project's calendar the person who has a task stands
25
+ // before its title, since the days hold several people's work. Who has what says whether the
26
+ // work is on its days. Takes its data as props and nothing from the network; the month or the
27
+ // week shown is the address's (ADR-0010).
27
28
 
28
29
  const isOpen = (task: RestTask): boolean => OPEN_TASK_STATES.includes(task.state);
29
30
 
@@ -104,49 +105,67 @@ function push(map: Map<string, RestTask[]>, day: string, task: RestTask): void {
104
105
  }
105
106
  }
106
107
 
107
- /** The tasks that sit on one day each: due on it, or beginning on it with no due day yet. */
108
- function onDays(tasks: readonly RestTask[]): {
109
- readonly due: ReadonlyMap<string, readonly RestTask[]>;
110
- readonly starting: ReadonlyMap<string, readonly RestTask[]>;
111
- } {
112
- const due = new Map<string, RestTask[]>();
113
- const starting = new Map<string, RestTask[]>();
108
+ /** The tasks that sit on one day each, by day: due on it, or beginning on it with no due day yet. */
109
+ function onDays(tasks: readonly RestTask[]): ReadonlyMap<string, readonly RestTask[]> {
110
+ const on = new Map<string, RestTask[]>();
114
111
  for (const task of tasks) {
115
112
  if (spans(task)) {
116
113
  continue;
117
114
  }
118
- if (task.dueOn !== null) {
119
- push(due, task.dueOn, task);
120
- } else if (task.startsOn !== null) {
121
- push(starting, task.startsOn, task);
115
+ const day = task.dueOn ?? task.startsOn;
116
+ if (day !== null) {
117
+ push(on, day, task);
122
118
  }
123
119
  }
124
120
  // Within a day, what is still open comes first; the rest reads as what was.
125
- for (const list of due.values()) {
121
+ for (const list of on.values()) {
126
122
  list.sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || a.number - b.number);
127
123
  }
128
- return { due, starting };
124
+ return on;
129
125
  }
130
126
 
131
- /** How a task reads on a day: struck through once it is over, and marked once it is late. */
132
- const tone = (task: RestTask, day: string, now: string): string =>
133
- cx(!isOpen(task) && "sc-calendar-over", isOpen(task) && day < now && "sc-calendar-late");
127
+ /** How a task reads: struck through once it is over, and marked once its due day is past. */
128
+ const tone = (task: RestTask, now: string): string =>
129
+ cx(
130
+ !isOpen(task) && "sc-calendar-over",
131
+ isOpen(task) && task.dueOn !== null && task.dueOn < now && "sc-calendar-late",
132
+ );
134
133
 
135
134
  interface WeekRowProps {
136
135
  readonly week: readonly GridDay[];
137
136
  readonly segments: readonly WeekSegment[];
138
- readonly due: ReadonlyMap<string, readonly RestTask[]>;
139
- readonly starting: ReadonlyMap<string, readonly RestTask[]>;
137
+ /** The tasks on one day each, by day. */
138
+ readonly on: ReadonlyMap<string, readonly RestTask[]>;
140
139
  readonly now: string;
141
140
  readonly taskHref: (task: RestTask) => string;
142
141
  /** What stands before a task's title where the tasks are of several projects: the project's mark. */
143
142
  readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
143
+ /** Whether the person who has a task stands before its title, where the days hold several people's work. */
144
+ readonly people?: boolean | undefined;
145
+ }
146
+
147
+ /**
148
+ * Marks an item whose title its line cut short, or which shows none (a bar on its later days),
149
+ * so that the tip it carries in `data-tip` is shown at once while the pointer is on it. Measured
150
+ * as the pointer arrives, since only the browser knows where the line ended.
151
+ */
152
+ function reveal(event: SyntheticEvent<HTMLElement>): void {
153
+ const item = event.currentTarget;
154
+ const title = item.querySelector(".sc-calendar-task-title");
155
+ item.toggleAttribute("data-clipped", title === null || title.scrollWidth > title.clientWidth);
144
156
  }
145
157
 
146
158
  /** One week as a row of cells: the bars across the days in their lanes, then the tasks on each day. */
147
159
  function WeekRow(props: WeekRowProps) {
148
- const words = useWords().calendar;
149
160
  const { week, segments, now } = props;
161
+ const tip = (task: RestTask): string =>
162
+ props.people === true && task.assignee !== null
163
+ ? `#${task.number} ${task.title} \u00B7 ${personName(task.assignee)}`
164
+ : `#${task.number} ${task.title}`;
165
+ const who = (task: RestTask): ReactNode =>
166
+ props.people === true && task.assignee !== null ? (
167
+ <Avatar person={task.assignee} size="xs" />
168
+ ) : null;
150
169
  const lanes = segments.reduce((most, segment) => Math.max(most, segment.lane + 1), 0);
151
170
  return (
152
171
  <tr>
@@ -179,28 +198,34 @@ function WeekRow(props: WeekRowProps) {
179
198
  `sc-category-${task.category?.color ?? "gray"}`,
180
199
  segment.from === column && segment.begins && "sc-calendar-bar-begins",
181
200
  segment.to === column && segment.ends && "sc-calendar-bar-ends",
182
- tone(task, task.dueOn ?? cell.day, now),
201
+ tone(task, now),
202
+ "sc-tip",
183
203
  )}
184
204
  href={props.taskHref(task)}
185
- title={`#${task.number} ${task.title}`}
205
+ data-tip={tip(task)}
206
+ onMouseEnter={reveal}
207
+ onFocus={reveal}
186
208
  >
187
209
  {segment.from === column && (
188
210
  <>
189
211
  {props.markOf?.(task)}
212
+ {who(task)}
190
213
  <span className="sc-calendar-task-title">{task.title}</span>
191
214
  </>
192
215
  )}
193
216
  </a>
194
217
  );
195
218
  })}
196
- {(props.due.get(cell.day) ?? []).map((task) => {
219
+ {(props.on.get(cell.day) ?? []).map((task) => {
197
220
  const mark = props.markOf?.(task);
198
221
  return (
199
222
  <a
200
223
  key={task.id}
201
- className={cx("sc-calendar-task", tone(task, cell.day, now))}
224
+ className={cx("sc-calendar-task", tone(task, now), "sc-tip")}
202
225
  href={props.taskHref(task)}
203
- title={`#${task.number} ${task.title}`}
226
+ data-tip={tip(task)}
227
+ onMouseEnter={reveal}
228
+ onFocus={reveal}
204
229
  >
205
230
  {mark === undefined || mark === null ? (
206
231
  <span
@@ -212,21 +237,11 @@ function WeekRow(props: WeekRowProps) {
212
237
  ) : (
213
238
  mark
214
239
  )}
240
+ {who(task)}
215
241
  <span className="sc-calendar-task-title">{task.title}</span>
216
242
  </a>
217
243
  );
218
244
  })}
219
- {(props.starting.get(cell.day) ?? []).map((task) => (
220
- <a
221
- key={task.id}
222
- className="sc-calendar-task sc-calendar-task-start"
223
- href={props.taskHref(task)}
224
- title={`#${task.number} ${task.title}: ${words.startsOnly}`}
225
- >
226
- {props.markOf?.(task)}
227
- <span className="sc-calendar-task-title">{task.title}</span>
228
- </a>
229
- ))}
230
245
  </td>
231
246
  ))}
232
247
  </tr>
@@ -239,12 +254,11 @@ export function Calendar(props: CalendarProps) {
239
254
  const now = props.today ?? todayOf();
240
255
  const firstDay = firstDayOfWeek(tag);
241
256
  const weeks = monthGrid(props.month, firstDay);
242
- const { due, starting } = onDays(props.tasks);
257
+ const on = onDays(props.tasks);
243
258
  const inMonth = (day: string) => monthOf(day) === props.month;
244
259
  const monthWeeks = weeks.map((week) => ({ week, segments: weekSegments(week, props.tasks) }));
245
260
  const nothing =
246
- [...due.keys()].every((day) => !inMonth(day)) &&
247
- [...starting.keys()].every((day) => !inMonth(day)) &&
261
+ [...on.keys()].every((day) => !inMonth(day)) &&
248
262
  monthWeeks.every(({ week, segments }) =>
249
263
  segments.every(
250
264
  (segment) => !week.slice(segment.from, segment.to + 1).some((cell) => cell.inMonth),
@@ -293,10 +307,10 @@ export function Calendar(props: CalendarProps) {
293
307
  key={week[0]?.day}
294
308
  week={week}
295
309
  segments={segments}
296
- due={due}
297
- starting={starting}
310
+ on={on}
298
311
  now={now}
299
312
  taskHref={props.taskHref}
313
+ people
300
314
  />
301
315
  ))}
302
316
  </tbody>
@@ -341,9 +355,9 @@ export function Week(props: WeekProps) {
341
355
  const first = days[0] ?? now;
342
356
  const last = days[days.length - 1] ?? now;
343
357
  const week: GridDay[] = days.map((day) => ({ day, inMonth: true }));
344
- const { due, starting } = onDays(props.tasks);
358
+ const on = onDays(props.tasks);
345
359
  const segments = weekSegments(week, props.tasks);
346
- const nothing = segments.length === 0 && days.every((day) => !due.has(day) && !starting.has(day));
360
+ const nothing = segments.length === 0 && days.every((day) => !on.has(day));
347
361
  return (
348
362
  <div className="sc-calendar sc-week">
349
363
  <div className="sc-calendar-head">
@@ -382,8 +396,7 @@ export function Week(props: WeekProps) {
382
396
  <WeekRow
383
397
  week={week}
384
398
  segments={segments}
385
- due={due}
386
- starting={starting}
399
+ on={on}
387
400
  now={now}
388
401
  taskHref={props.taskHref}
389
402
  markOf={props.markOf}
@@ -2,7 +2,7 @@ import { useState } from "react";
2
2
  import { normalizeConnectCode, type RestConnectRequest, type RestTokenInput } from "../api.js";
3
3
  import { useWords } from "../i18n/index.js";
4
4
  import { TokenForm } from "./tokens.js";
5
- import { Button, Callout, Time } from "./ui.js";
5
+ import { Button, Callout, CopyButton, Time } from "./ui.js";
6
6
 
7
7
  // Connecting an agent (ADR-0011): where a person types the code their agent showed, what asks
8
8
  // to connect under a code with the way to approve it, and the words that tell a person what to
@@ -106,16 +106,22 @@ export interface ConnectWordsProps {
106
106
  readonly origin: string;
107
107
  }
108
108
 
109
- /** What a person says to their agent to connect it, in words for someone who is not a developer. */
109
+ /**
110
+ * What a person says to their agent to connect it: the words to paste into its chat, as they
111
+ * are, with the way to copy them; and the commands for a person who runs them by hand.
112
+ */
110
113
  export function ConnectWords(props: ConnectWordsProps) {
111
114
  const words = useWords().connect.words;
115
+ const quote = words.quote(props.origin);
112
116
  return (
113
117
  <section className="sc-panel sc-connect" aria-label={words.title}>
114
118
  <h2 className="sc-section-title">{words.title}</h2>
115
- <p>
116
- {words.tellYourAgent} <q>{words.quote(props.origin)}</q> {words.doesTheRest}
117
- </p>
118
- <p>{words.thenApprove}</p>
119
+ <p>{words.tellYourAgent}</p>
120
+ <div className="sc-copyable">
121
+ <pre className="sc-code">{quote}</pre>
122
+ <CopyButton text={quote} size="sm" />
123
+ </div>
124
+ <p>{words.doesTheRest}</p>
119
125
  <p>{words.byHand}</p>
120
126
  <pre className="sc-code">
121
127
  npm install -g @skillcdn/console{"\n"}console login --url {props.origin}
@@ -1,5 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import {
3
+ agentLabel,
3
4
  MAX_BODY_LENGTH,
4
5
  MAX_NOTE_LENGTH,
5
6
  type RestAnswerInput,
@@ -128,7 +129,7 @@ export function DecisionCard(props: DecisionCardProps) {
128
129
  <PersonChip person={decision.raisedBy} />{" "}
129
130
  {decision.run === null
130
131
  ? words.decisions.asked
131
- : words.decisions.askedThrough(decision.run.agent)}{" "}
132
+ : words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
132
133
  <Time iso={decision.createdAt} />
133
134
  {task !== undefined && (
134
135
  <>
@@ -1,4 +1,5 @@
1
1
  import { type FormEvent, type ReactNode, useState } from "react";
2
+ import { agentLabel } from "../agent-name.js";
2
3
  import {
3
4
  foldersAbove,
4
5
  isDocumentPath,
@@ -59,7 +60,7 @@ export function DocumentCrumbs(props: {
59
60
  }
60
61
 
61
62
  /** Who wrote a version, and through which agent, as a chip and a word. */
62
- function Writer(props: {
63
+ export function Writer(props: {
63
64
  readonly by: RestDocumentSummary["updatedBy"];
64
65
  readonly agent: string | null;
65
66
  }) {
@@ -68,8 +69,11 @@ function Writer(props: {
68
69
  <>
69
70
  <PersonChip person={props.by} />
70
71
  {props.agent !== null && (
71
- <span className="sc-feed-agent" title={words.docs.asAgentTitle}>
72
- {words.feed.as(props.agent)}
72
+ <span
73
+ className="sc-feed-agent"
74
+ title={agentLabel(props.agent) === props.agent ? words.docs.asAgentTitle : props.agent}
75
+ >
76
+ {words.feed.as(agentLabel(props.agent))}
73
77
  </span>
74
78
  )}
75
79
  </>
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { RestEvent } from "../api.js";
2
+ import { agentLabel, type RestEvent } from "../api.js";
3
3
  import { en, type Messages } from "../i18n/en.js";
4
4
  import { useWords } from "../i18n/index.js";
5
5
  import { ShowMore, usePaged } from "./paged.js";
@@ -38,6 +38,14 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
38
38
  return feed.event.personJoined();
39
39
  case "person.role_changed":
40
40
  return feed.event.roleChanged(login, role(data.role));
41
+ case "person.listed":
42
+ return feed.event.personListed(login);
43
+ case "person.unlisted":
44
+ return feed.event.personUnlisted(login);
45
+ case "person.deactivated":
46
+ return feed.event.personDeactivated(login);
47
+ case "person.reactivated":
48
+ return feed.event.personReactivated(login);
41
49
  case "project.created":
42
50
  return feed.event.projectCreated(project);
43
51
  case "project.updated":
@@ -129,8 +137,13 @@ export function EventFeed(props: EventFeedProps) {
129
137
  <PersonChip person={event.actor} />
130
138
  )}
131
139
  {event.agent !== null && (
132
- <span className="sc-feed-agent" title={words.feed.asTitle}>
133
- {words.feed.as(event.agent)}
140
+ <span
141
+ className="sc-feed-agent"
142
+ title={
143
+ agentLabel(event.agent) === event.agent ? words.feed.asTitle : event.agent
144
+ }
145
+ >
146
+ {words.feed.as(agentLabel(event.agent))}
134
147
  </span>
135
148
  )}
136
149
  </span>