@skillcdn/console 0.1.10 → 0.1.11

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 (132) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +6 -5
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +5 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +49 -9
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +2 -0
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +7 -6
  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 +10 -9
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +14 -5
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +13 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/components/board.d.ts +1 -4
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +5 -12
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/calendar.d.ts +21 -1
  25. package/dist/components/calendar.d.ts.map +1 -1
  26. package/dist/components/calendar.js +67 -7
  27. package/dist/components/calendar.js.map +1 -1
  28. package/dist/components/decision-list.d.ts.map +1 -1
  29. package/dist/components/decision-list.js +11 -9
  30. package/dist/components/decision-list.js.map +1 -1
  31. package/dist/components/event-feed.d.ts.map +1 -1
  32. package/dist/components/event-feed.js +11 -7
  33. package/dist/components/event-feed.js.map +1 -1
  34. package/dist/components/paged.d.ts +14 -0
  35. package/dist/components/paged.d.ts.map +1 -0
  36. package/dist/components/paged.js +28 -0
  37. package/dist/components/paged.js.map +1 -0
  38. package/dist/components/projects.d.ts +22 -2
  39. package/dist/components/projects.d.ts.map +1 -1
  40. package/dist/components/projects.js +35 -7
  41. package/dist/components/projects.js.map +1 -1
  42. package/dist/components/runs.d.ts.map +1 -1
  43. package/dist/components/runs.js +2 -2
  44. package/dist/components/runs.js.map +1 -1
  45. package/dist/components/shell.d.ts +2 -1
  46. package/dist/components/shell.d.ts.map +1 -1
  47. package/dist/components/shell.js +1 -1
  48. package/dist/components/shell.js.map +1 -1
  49. package/dist/components/task-list.d.ts +1 -1
  50. package/dist/components/task-list.d.ts.map +1 -1
  51. package/dist/components/task-list.js +6 -4
  52. package/dist/components/task-list.js.map +1 -1
  53. package/dist/components/task-view.js +1 -1
  54. package/dist/components/task-view.js.map +1 -1
  55. package/dist/components/work.d.ts.map +1 -1
  56. package/dist/components/work.js +7 -3
  57. package/dist/components/work.js.map +1 -1
  58. package/dist/console.css +149 -2
  59. package/dist/console.d.ts.map +1 -1
  60. package/dist/console.js +34 -18
  61. package/dist/console.js.map +1 -1
  62. package/dist/data.d.ts +5 -2
  63. package/dist/data.d.ts.map +1 -1
  64. package/dist/data.js +15 -2
  65. package/dist/data.js.map +1 -1
  66. package/dist/i18n/en.d.ts +12 -6
  67. package/dist/i18n/en.d.ts.map +1 -1
  68. package/dist/i18n/en.js +12 -8
  69. package/dist/i18n/en.js.map +1 -1
  70. package/dist/i18n/ko.d.ts.map +1 -1
  71. package/dist/i18n/ko.js +12 -8
  72. package/dist/i18n/ko.js.map +1 -1
  73. package/dist/index.d.ts +3 -2
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +3 -2
  76. package/dist/index.js.map +1 -1
  77. package/dist/limits.d.ts +6 -0
  78. package/dist/limits.d.ts.map +1 -1
  79. package/dist/limits.js +6 -0
  80. package/dist/limits.js.map +1 -1
  81. package/dist/marks.d.ts +12 -0
  82. package/dist/marks.d.ts.map +1 -0
  83. package/dist/marks.js +31 -0
  84. package/dist/marks.js.map +1 -0
  85. package/dist/router.d.ts +4 -3
  86. package/dist/router.d.ts.map +1 -1
  87. package/dist/router.js +11 -5
  88. package/dist/router.js.map +1 -1
  89. package/dist/routes.d.ts +7 -0
  90. package/dist/routes.d.ts.map +1 -1
  91. package/dist/routes.js +8 -0
  92. package/dist/routes.js.map +1 -1
  93. package/dist/schemas.d.ts +123 -3
  94. package/dist/schemas.d.ts.map +1 -1
  95. package/dist/schemas.js +42 -10
  96. package/dist/schemas.js.map +1 -1
  97. package/dist/vocabulary.d.ts +10 -4
  98. package/dist/vocabulary.d.ts.map +1 -1
  99. package/dist/vocabulary.js +8 -3
  100. package/dist/vocabulary.js.map +1 -1
  101. package/docs/specs/cli.md +7 -5
  102. package/docs/specs/rest.md +18 -12
  103. package/package.json +1 -1
  104. package/skills/working-the-board/SKILL.md +1 -1
  105. package/src/api.ts +12 -0
  106. package/src/cli/cli.ts +51 -8
  107. package/src/cli/format.ts +8 -6
  108. package/src/cli/help.ts +10 -10
  109. package/src/client.ts +29 -6
  110. package/src/components/board.tsx +5 -38
  111. package/src/components/calendar.tsx +166 -54
  112. package/src/components/decision-list.tsx +38 -33
  113. package/src/components/event-feed.tsx +34 -27
  114. package/src/components/paged.tsx +39 -0
  115. package/src/components/projects.tsx +128 -10
  116. package/src/components/runs.tsx +7 -1
  117. package/src/components/shell.tsx +5 -2
  118. package/src/components/task-list.tsx +35 -32
  119. package/src/components/task-view.tsx +1 -1
  120. package/src/components/work.tsx +12 -2
  121. package/src/console.tsx +54 -20
  122. package/src/data.ts +20 -4
  123. package/src/i18n/en.ts +12 -8
  124. package/src/i18n/ko.ts +12 -8
  125. package/src/index.ts +4 -0
  126. package/src/limits.ts +6 -0
  127. package/src/marks.ts +40 -0
  128. package/src/router.ts +19 -7
  129. package/src/routes.ts +9 -0
  130. package/src/schemas.ts +48 -8
  131. package/src/styles/console.css +149 -2
  132. package/src/vocabulary.ts +14 -4
package/src/data.ts CHANGED
@@ -58,10 +58,13 @@ const FEED_MAX_PAGES = 20;
58
58
  const RELOAD_DELAY_MS = 150;
59
59
 
60
60
  export interface ConsoleActions {
61
- /** Makes a project, whose owner the person becomes. */
62
- createProject(input: RestProjectInput): Promise<RestProject>;
61
+ /** Makes a project, whose owner the person becomes, with an image for its mark when one is given. */
62
+ createProject(input: RestProjectInput, icon?: FileUpload): Promise<RestProject>;
63
63
  /** Changes the settings of the project the page is on; for an owner. */
64
64
  updateProject(patch: RestProjectPatch): Promise<RestProject>;
65
+ /** Sets the image for the mark of the project the page is on, or takes it away; for an owner. */
66
+ setProjectIcon(input: FileUpload): Promise<RestProject>;
67
+ removeProjectIcon(): Promise<RestProject>;
65
68
  addMember(input: RestMemberInput): Promise<RestMember>;
66
69
  updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
67
70
  removeMember(personId: string): Promise<void>;
@@ -387,10 +390,11 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
387
390
  return client.project(projectKey);
388
391
  };
389
392
  return {
390
- async createProject(input) {
393
+ async createProject(input, icon) {
391
394
  const made = await client.createProject(input);
395
+ const marked = icon === undefined ? made : await client.project(made.key).setIcon(icon);
392
396
  await refreshWorkspace();
393
- return made;
397
+ return marked;
394
398
  },
395
399
  async updateProject(patch) {
396
400
  const changed = await scope().update(patch);
@@ -398,6 +402,18 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
398
402
  await refreshWorkspace();
399
403
  return changed;
400
404
  },
405
+ async setProjectIcon(input) {
406
+ const changed = await scope().setIcon(input);
407
+ setProject(changed);
408
+ await refreshWorkspace();
409
+ return changed;
410
+ },
411
+ async removeProjectIcon() {
412
+ const changed = await scope().removeIcon();
413
+ setProject(changed);
414
+ await refreshWorkspace();
415
+ return changed;
416
+ },
401
417
  async addMember(input) {
402
418
  const member = await scope().addMember(input);
403
419
  await refreshProject();
package/src/i18n/en.ts CHANGED
@@ -43,7 +43,7 @@ export const en = {
43
43
  private: "Only those listed",
44
44
  } satisfies Record<ProjectVisibility, string>,
45
45
  runStatus: {
46
- running: "At work",
46
+ running: "Open",
47
47
  waiting: "Waiting for a decision",
48
48
  finished: "Finished",
49
49
  failed: "Failed",
@@ -95,6 +95,7 @@ export const en = {
95
95
  nobody: "Nobody",
96
96
  nothing: "Nothing",
97
97
  language: "Language",
98
+ showMore: (n: number, of: number) => `Show ${n} more of ${of}`,
98
99
  },
99
100
 
100
101
  nav: {
@@ -150,7 +151,6 @@ export const en = {
150
151
  list: "Projects",
151
152
  private: "Private",
152
153
  decisionsWaiting: (n: number) => `${count(n, "decision", "decisions")} waiting`,
153
- agentsAtWork: (n: number) => `${count(n, "agent", "agents")} at work`,
154
154
  form: {
155
155
  aria: "New project",
156
156
  name: "Name",
@@ -162,6 +162,11 @@ export const en = {
162
162
  whoIsMember: "Who is a member",
163
163
  skillsAddress: "Skills address",
164
164
  skillsAddressPlaceholder: "/gh/<owner>/<repo>, or empty for the organization's",
165
+ symbol: "Mark",
166
+ image: "Image",
167
+ imageRemove: "Remove the image",
168
+ color: "Colour",
169
+ colorAuto: "Chosen from the key",
165
170
  make: "Make the project",
166
171
  },
167
172
  members: {
@@ -207,9 +212,6 @@ export const en = {
207
212
  moveTo: "Move to",
208
213
  decisions: (n: number) => count(n, "decision", "decisions"),
209
214
  decisionsTitle: "Decisions waiting for a person",
210
- agentAtWork: "agent at work",
211
- agentAtWorkTitle: "An agent is at work on it",
212
- agentSince: (agent: string, since: string) => `${agent}, at work since ${since}`,
213
215
  filters: {
214
216
  aria: "Narrow the board",
215
217
  everyone: "Everyone",
@@ -247,7 +249,6 @@ export const en = {
247
249
  raiseDecision: "Raise a decision",
248
250
  nothingMore: "Nothing more was written about it.",
249
251
  links: "Links",
250
- agentsAtWork: "Agents at work",
251
252
  runs: "Runs",
252
253
  decisions: "Decisions",
253
254
  history: "What happened",
@@ -336,8 +337,10 @@ export const en = {
336
337
 
337
338
  calendar: {
338
339
  title: "Calendar",
339
- lead: "The tasks by the day they are due. A task with a start is shown from it; one with no day waits at the bottom to be given one.",
340
+ lead: "The tasks on their days: planned from a start to a due day, a task lies across them; due on a day alone, it sits on it. One with no day waits at the bottom to be given one.",
340
341
  grid: "The month",
342
+ legend: { title: "Category colours" },
343
+ startsOnly: "Starts that day, with no due day yet",
341
344
  previous: "The month before",
342
345
  next: "The month after",
343
346
  thisMonth: "This month",
@@ -346,7 +349,6 @@ export const en = {
346
349
  nothingOverdue: "Nothing is overdue.",
347
350
  unscheduled: "No day set",
348
351
  allScheduled: "Every open task has a day.",
349
- starts: "starts",
350
352
  workload: {
351
353
  title: "Who has what",
352
354
  lead: "The open tasks of each person, and what of them is late or due within the week.",
@@ -509,6 +511,7 @@ export const en = {
509
511
  runReported: (task: string, excerpt: string | undefined) =>
510
512
  `reported on ${task}${excerpt === undefined ? "" : `: ${excerpt}`}`,
511
513
  runHandedIn: (label: string, task: string) => `handed in ${label} on ${task}`,
514
+ runTakenOver: (task: string, agent: string) => `took over ${task} from ${agent}`,
512
515
  runFinished: (task: string) => `finished ${task}`,
513
516
  runFailed: (task: string) => `failed on ${task}`,
514
517
  runAbandoned: (task: string) => `gave up on ${task}`,
@@ -524,6 +527,7 @@ export const en = {
524
527
  for: "for",
525
528
  since: "since",
526
529
  until: "until",
530
+ lastHeard: "last heard",
527
531
  markAbandoned: "Mark abandoned",
528
532
  waitingForDecision: "Waiting for a decision",
529
533
  answerIt: "answer it",
package/src/i18n/ko.ts CHANGED
@@ -21,7 +21,7 @@ export const ko: Messages = {
21
21
  projectRole: { owner: "소유자", member: "멤버" },
22
22
  visibility: { workspace: "워크스페이스의 모두", private: "목록에 있는 사람만" },
23
23
  runStatus: {
24
- running: "작업 중",
24
+ running: "열림",
25
25
  waiting: "결정 대기 중",
26
26
  finished: "완료",
27
27
  failed: "실패",
@@ -68,6 +68,7 @@ export const ko: Messages = {
68
68
  nobody: "없음",
69
69
  nothing: "없음",
70
70
  language: "언어",
71
+ showMore: (n, of) => `${of}개 중 ${n}개 더 보기`,
71
72
  },
72
73
 
73
74
  nav: {
@@ -123,7 +124,6 @@ export const ko: Messages = {
123
124
  list: "프로젝트",
124
125
  private: "비공개",
125
126
  decisionsWaiting: (n) => `결정 ${n}건 대기 중`,
126
- agentsAtWork: (n) => `에이전트 ${n}개 작업 중`,
127
127
  form: {
128
128
  aria: "새 프로젝트",
129
129
  name: "이름",
@@ -134,6 +134,11 @@ export const ko: Messages = {
134
134
  whoIsMember: "멤버 범위",
135
135
  skillsAddress: "스킬 주소",
136
136
  skillsAddressPlaceholder: "/gh/<owner>/<repo>, 비우면 조직의 스킬",
137
+ symbol: "마크",
138
+ image: "이미지",
139
+ imageRemove: "이미지 지우기",
140
+ color: "색",
141
+ colorAuto: "키에서 정해진 색",
137
142
  make: "프로젝트 만들기",
138
143
  },
139
144
  members: {
@@ -179,9 +184,6 @@ export const ko: Messages = {
179
184
  moveTo: "이동",
180
185
  decisions: (n) => `결정 ${n}건`,
181
186
  decisionsTitle: "사람을 기다리는 결정",
182
- agentAtWork: "에이전트 작업 중",
183
- agentAtWorkTitle: "에이전트가 이 작업을 하고 있습니다",
184
- agentSince: (agent, since) => `${agent}, ${since}부터 작업 중`,
185
187
  filters: {
186
188
  aria: "보드 좁혀 보기",
187
189
  everyone: "모두",
@@ -219,7 +221,6 @@ export const ko: Messages = {
219
221
  raiseDecision: "결정 요청",
220
222
  nothingMore: "더 적힌 내용이 없습니다.",
221
223
  links: "링크",
222
- agentsAtWork: "작업 중인 에이전트",
223
224
  runs: "실행",
224
225
  decisions: "결정",
225
226
  history: "일어난 일",
@@ -308,8 +309,10 @@ export const ko: Messages = {
308
309
 
309
310
  calendar: {
310
311
  title: "캘린더",
311
- lead: "작업을 기한일에 놓고 봅니다. 시작일이 있는 작업은 그날부터 보이고, 날짜가 없는 작업은 아래에서 날짜를 기다립니다.",
312
+ lead: "작업을 날짜 위에 놓고 봅니다. 시작일과 기한이 있는 작업은 그 기간에 걸쳐 놓이고, 기한만 있는 작업은 그날에 놓입니다. 날짜가 없는 작업은 아래에서 날짜를 기다립니다.",
312
313
  grid: "이 달",
314
+ legend: { title: "카테고리 색" },
315
+ startsOnly: "그날 시작, 기한은 아직 없음",
313
316
  previous: "이전 달",
314
317
  next: "다음 달",
315
318
  thisMonth: "이번 달",
@@ -318,7 +321,6 @@ export const ko: Messages = {
318
321
  nothingOverdue: "기한이 지난 작업이 없습니다.",
319
322
  unscheduled: "날짜 없음",
320
323
  allScheduled: "열린 작업마다 날짜가 있습니다.",
321
- starts: "시작",
322
324
  workload: {
323
325
  title: "누가 무엇을",
324
326
  lead: "사람마다 열린 작업과, 그중 늦은 것과 일주일 안에 마감인 것.",
@@ -477,6 +479,7 @@ export const ko: Messages = {
477
479
  runReported: (task, excerpt) =>
478
480
  `${task}에 보고했습니다${excerpt === undefined ? "" : `: ${excerpt}`}`,
479
481
  runHandedIn: (label, task) => `${task}에 ${label}을(를) 제출했습니다`,
482
+ runTakenOver: (task, agent) => `${agent}에게서 ${task}을(를) 넘겨받았습니다`,
480
483
  runFinished: (task) => `${task}을(를) 끝냈습니다`,
481
484
  runFailed: (task) => `${task}에 실패했습니다`,
482
485
  runAbandoned: (task) => `${task}을(를) 포기했습니다`,
@@ -492,6 +495,7 @@ export const ko: Messages = {
492
495
  for: "대신:",
493
496
  since: "시작",
494
497
  until: "종료",
498
+ lastHeard: "마지막 소식",
495
499
  markAbandoned: "중단됨으로 표시",
496
500
  waitingForDecision: "결정을 기다리는 중",
497
501
  answerIt: "답변하기",
package/src/index.ts CHANGED
@@ -16,9 +16,11 @@ export {
16
16
  export {
17
17
  Calendar,
18
18
  type CalendarProps,
19
+ type WeekSegment,
19
20
  Workload,
20
21
  type WorkloadProps,
21
22
  type WorkloadRow,
23
+ weekSegments,
22
24
  workloadOf,
23
25
  } from "./components/calendar.js";
24
26
  export { CategoryList, type CategoryListProps } from "./components/categories.js";
@@ -64,6 +66,7 @@ export {
64
66
  type GoalViewProps,
65
67
  } from "./components/goals.js";
66
68
  export { Markdown, type MarkdownProps } from "./components/markdown.js";
69
+ export { PAGE_SIZE, ShowMore, usePaged } from "./components/paged.js";
67
70
  export { PeopleList, type PeopleListProps } from "./components/people.js";
68
71
  export {
69
72
  keyOf,
@@ -74,6 +77,7 @@ export {
74
77
  type ProjectFormProps,
75
78
  ProjectList,
76
79
  type ProjectListProps,
80
+ ProjectMark,
77
81
  ProjectRoleBadge,
78
82
  VISIBILITY_LABELS,
79
83
  } from "./components/projects.js";
package/src/limits.ts CHANGED
@@ -64,3 +64,9 @@ export const MAX_CATEGORY_KEY_LENGTH = 40;
64
64
  export const MAX_CATEGORY_NAME_LENGTH = 60;
65
65
  /** How many categories one workspace may have: a vocabulary, not a tag cloud. */
66
66
  export const MAX_CATEGORIES = 50;
67
+ /** A project's mark: a letter or two, or a symbol, shown on its colour; counted in code units. */
68
+ export const MAX_PROJECT_SYMBOL_LENGTH = 8;
69
+ /** How large a project's own image for its mark may be, in bytes. */
70
+ export const MAX_PROJECT_ICON_BYTES = 512 * 1024;
71
+ /** How many items a listing answers with when none is asked for, and the most one may ask for. */
72
+ export const DEFAULT_PAGE_LIMIT = 50;
package/src/marks.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { COLOR_NAMES, type ColorName } from "./vocabulary.js";
2
+
3
+ // A project's mark: a symbol on a colour, so that projects tell apart at a glance. A project
4
+ // that names neither gets a colour from its key, the same everywhere and every time, and the
5
+ // first letter of its name.
6
+
7
+ /** A small, well-spread hash of a string: enough to pick a colour, nothing more. */
8
+ function hashOf(value: string): number {
9
+ let hash = 2166136261;
10
+ for (const char of value) {
11
+ hash ^= char.codePointAt(0) ?? 0;
12
+ hash = Math.imul(hash, 16777619) >>> 0;
13
+ }
14
+ return hash;
15
+ }
16
+
17
+ /** The colour of a project's mark: the one it names, else one chosen from its key. */
18
+ export function projectColorOf(project: {
19
+ readonly key: string;
20
+ readonly color: ColorName | null;
21
+ }): ColorName {
22
+ if (project.color !== null) {
23
+ return project.color;
24
+ }
25
+ // Gray is for what has no colour; a project chosen one gets one of the rest.
26
+ const choices = COLOR_NAMES.filter((name) => name !== "gray");
27
+ return choices[hashOf(project.key) % choices.length] ?? "gray";
28
+ }
29
+
30
+ /** The symbol of a project's mark: the one it names, else the first letter of its name. */
31
+ export function projectSymbolOf(project: {
32
+ readonly name: string;
33
+ readonly symbol: string | null;
34
+ }): string {
35
+ if (project.symbol !== null && project.symbol.length > 0) {
36
+ return project.symbol;
37
+ }
38
+ const first = [...project.name.trim()][0];
39
+ return first === undefined ? "?" : first.toUpperCase();
40
+ }
package/src/router.ts CHANGED
@@ -52,8 +52,13 @@ export type Route =
52
52
  | { readonly name: "decisions"; readonly project: string }
53
53
  | { readonly name: "new-decision"; readonly project: string }
54
54
  | { readonly name: "decision"; readonly project: string; readonly id: string }
55
- /** The calendar of a project: the month shown, or this one when the address names none. */
56
- | { readonly name: "calendar"; readonly project: string; readonly month: string | undefined }
55
+ /** The calendar of a project: the month shown, or this one when the address names none, narrowed as the board is. */
56
+ | {
57
+ readonly name: "calendar";
58
+ readonly project: string;
59
+ readonly month: string | undefined;
60
+ readonly filter: BoardFilter;
61
+ }
57
62
  | { readonly name: "roadmap"; readonly project: string }
58
63
  | { readonly name: "new-goal"; readonly project: string }
59
64
  | {
@@ -280,7 +285,7 @@ export function matchRoute(pathname: string, search = ""): Route {
280
285
  if (month !== null && !isMonth(month)) {
281
286
  return NOT_FOUND;
282
287
  }
283
- return { name: "calendar", project, month: month ?? undefined };
288
+ return { name: "calendar", project, month: month ?? undefined, filter: boardFilterOf(params) };
284
289
  }
285
290
  if (page === "roadmap") {
286
291
  return { name: "roadmap", project };
@@ -307,6 +312,12 @@ export function projectHref(key: string, page?: ProjectPage): string {
307
312
 
308
313
  /** Where a project's board is, narrowed as the filter says; the plain board for an empty one. */
309
314
  export function boardHref(key: string, filter: BoardFilter = {}): string {
315
+ const parts = filterParts(filter);
316
+ return parts.length === 0 ? projectHref(key) : `${projectHref(key)}?${parts.join("&")}`;
317
+ }
318
+
319
+ /** The filter as the query carries it, a part per narrowing; nothing for the plain board. */
320
+ function filterParts(filter: BoardFilter): string[] {
310
321
  const parts: string[] = [];
311
322
  // An empty value means "none": it keeps its sign; "mine" is a flag and has no value.
312
323
  if (filter.category !== undefined) {
@@ -321,7 +332,7 @@ export function boardHref(key: string, filter: BoardFilter = {}): string {
321
332
  if (filter.mine === true) {
322
333
  parts.push("mine");
323
334
  }
324
- return parts.length === 0 ? projectHref(key) : `${projectHref(key)}?${parts.join("&")}`;
335
+ return parts;
325
336
  }
326
337
 
327
338
  /** Where a task is written: the form. */
@@ -345,10 +356,11 @@ export function decisionHref(key: string, id: string): string {
345
356
  return `${PATHS.project}/${key}/decisions/${id}`;
346
357
  }
347
358
 
348
- /** Where a project's calendar is, on a month, or on this one when none is named. */
349
- export function calendarHref(key: string, month?: string): string {
359
+ /** Where a project's calendar is, on a month, or on this one when none is named, narrowed as the filter says. */
360
+ export function calendarHref(key: string, month?: string, filter: BoardFilter = {}): string {
350
361
  const base = projectHref(key, "calendar");
351
- return month === undefined ? base : `${base}?month=${month}`;
362
+ const parts = [...(month === undefined ? [] : [`month=${month}`]), ...filterParts(filter)];
363
+ return parts.length === 0 ? base : `${base}?${parts.join("&")}`;
352
364
  }
353
365
 
354
366
  /** Where a goal is set: the form. */
package/src/routes.ts CHANGED
@@ -114,3 +114,12 @@ export function projectPath(key: string, collection?: ProjectCollection, id?: st
114
114
  ? `${base}/${collection}`
115
115
  : `${base}/${collection}/${encodeURIComponent(id)}`;
116
116
  }
117
+
118
+ /**
119
+ * Where a project's own image for its mark is read, with the hash as a version where the image
120
+ * is known, so that a browser's copy is asked for again only when the image changes.
121
+ */
122
+ export function projectIconPath(key: string, icon?: { readonly sha256: string } | null): string {
123
+ const base = `${projectPath(key)}/icon`;
124
+ return icon === undefined || icon === null ? base : `${base}?v=${icon.sha256.slice(0, 16)}`;
125
+ }
package/src/schemas.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  MAX_PROJECT_DESCRIPTION_LENGTH,
22
22
  MAX_PROJECT_KEY_LENGTH,
23
23
  MAX_PROJECT_NAME_LENGTH,
24
+ MAX_PROJECT_SYMBOL_LENGTH,
24
25
  MAX_QUESTION_LENGTH,
25
26
  MAX_SECRET_LENGTH,
26
27
  MAX_SKILLS_ADDRESS_LENGTH,
@@ -35,10 +36,12 @@ import { hasForbiddenCodePoint } from "./text.js";
35
36
  import {
36
37
  ARTIFACT_KINDS,
37
38
  CATEGORY_COLORS,
39
+ COLOR_NAMES,
38
40
  EVENT_KINDS,
39
41
  GOAL_STATES,
40
42
  LINK_SOURCES,
41
43
  PERSON_ROLES,
44
+ PROJECT_ICON_TYPES,
42
45
  PROJECT_ROLES,
43
46
  PROJECT_VISIBILITIES,
44
47
  PROVIDER_KEYS,
@@ -160,7 +163,19 @@ const projectKey = z.string().check(
160
163
  /** The address of a project's skills, as the standard spells one; the console checks it as one. */
161
164
  const skillsAddress = line(MAX_SKILLS_ADDRESS_LENGTH);
162
165
 
166
+ /** A project's mark: a letter or two, or a symbol, with no space in it; shown on the project's colour. */
167
+ const projectSymbol = line(MAX_PROJECT_SYMBOL_LENGTH).check(
168
+ z.refine((value) => !/\s/.test(value), "must have no space in it"),
169
+ );
170
+
163
171
  /** A project of the workspace, as the asker sees it: with what they are in it. */
172
+ /** A project's own image for its mark: the hash it is kept by, which `projectIconPath` reads it with, and its kind. */
173
+ export const restProjectIconSchema = z.object({
174
+ sha256: z.string().check(z.regex(/^[0-9a-f]{64}$/)),
175
+ contentType: z.enum(PROJECT_ICON_TYPES),
176
+ });
177
+ export type RestProjectIcon = z.infer<typeof restProjectIconSchema>;
178
+
164
179
  export const restProjectSchema = z.object({
165
180
  id: uuid,
166
181
  key: projectKey,
@@ -170,6 +185,12 @@ export const restProjectSchema = z.object({
170
185
  visibility: z.enum(PROJECT_VISIBILITIES),
171
186
  /** The address of the project's skills at the SkillCDN deployment, canonical, or `null` for the organization's. */
172
187
  skillsAddress: z.nullable(z.string()),
188
+ /** The colour of the project's mark, one of the names, or `null` for one chosen from its key. */
189
+ color: z.nullable(z.enum(COLOR_NAMES)),
190
+ /** The project's mark, a letter or two or a symbol, or `null` for the first letter of its name. */
191
+ symbol: z.nullable(z.string()),
192
+ /** The project's own image for its mark, shown in place of the symbol, or `null` for none. */
193
+ icon: z.nullable(restProjectIconSchema),
173
194
  /** What the asker is in it: an owner configures the project, a member works on it. */
174
195
  role: z.enum(PROJECT_ROLES),
175
196
  /** How many decisions in it wait for a person. */
@@ -194,6 +215,9 @@ export const restProjectInputSchema = z.object({
194
215
  visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
195
216
  /** Left out or `null`: the organization's skills. */
196
217
  skillsAddress: z.optional(z.nullable(skillsAddress)),
218
+ /** Left out or `null`: a colour chosen from the key, and the first letter of the name. */
219
+ color: z.optional(z.nullable(z.enum(COLOR_NAMES))),
220
+ symbol: z.optional(z.nullable(projectSymbol)),
197
221
  });
198
222
  export type RestProjectInput = z.infer<typeof restProjectInputSchema>;
199
223
 
@@ -203,6 +227,8 @@ export const restProjectPatchSchema = z.object({
203
227
  description: z.optional(paragraphs(MAX_PROJECT_DESCRIPTION_LENGTH)),
204
228
  visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
205
229
  skillsAddress: z.optional(z.nullable(skillsAddress)),
230
+ color: z.optional(z.nullable(z.enum(COLOR_NAMES))),
231
+ symbol: z.optional(z.nullable(projectSymbol)),
206
232
  });
207
233
  export type RestProjectPatch = z.infer<typeof restProjectPatchSchema>;
208
234
 
@@ -313,8 +339,8 @@ export const restTaskSchema = z.object({
313
339
  });
314
340
  export type RestTask = z.infer<typeof restTaskSchema>;
315
341
 
316
- /** `GET /api/v1/projects/<key>/tasks`: the board, newest first. */
317
- export const restTasksSchema = z.object({ items: z.array(restTaskSchema) });
342
+ /** `GET /api/v1/projects/<key>/tasks`: a page of the board, newest first, and whether there is more after it. */
343
+ export const restTasksSchema = z.object({ items: z.array(restTaskSchema), more: z.boolean() });
318
344
  export type RestTasks = z.infer<typeof restTasksSchema>;
319
345
 
320
346
  const taskLinkInput = z.object({
@@ -449,12 +475,21 @@ export const restDecisionSchema = z.object({
449
475
  });
450
476
  export type RestDecision = z.infer<typeof restDecisionSchema>;
451
477
 
452
- /** `GET /api/v1/projects/<key>/decisions`: the ones that wait first, newest first within each. */
453
- export const restDecisionsSchema = z.object({ items: z.array(restDecisionSchema) });
478
+ /** `GET /api/v1/projects/<key>/decisions`: a page, the ones that wait first, newest first within each, and whether there is more. */
479
+ export const restDecisionsSchema = z.object({
480
+ items: z.array(restDecisionSchema),
481
+ more: z.boolean(),
482
+ });
454
483
  export type RestDecisions = z.infer<typeof restDecisionsSchema>;
455
484
 
456
- /** The project a thing on a person's plate belongs to, named for the page to say and link it. */
457
- const projectRef = z.object({ key: projectKey, name: z.string() });
485
+ /** The project a thing on a person's plate belongs to, named and marked for the page to say, link and show it. */
486
+ const projectRef = z.object({
487
+ key: projectKey,
488
+ name: z.string(),
489
+ color: z.nullable(z.enum(COLOR_NAMES)),
490
+ symbol: z.nullable(z.string()),
491
+ icon: z.nullable(restProjectIconSchema),
492
+ });
458
493
 
459
494
  /**
460
495
  * `GET /api/v1/work`: what is on the asker's plate across the projects they may see (ADR-0019):
@@ -598,6 +633,11 @@ export const restRunSchema = z.object({
598
633
  status: z.enum(RUN_STATUSES),
599
634
  startedAt: instant,
600
635
  endedAt: z.nullable(instant),
636
+ /**
637
+ * When the agent was last heard from on this run: when it began, reported, handed in or
638
+ * asked. The board cannot know whether an agent still runs; this is what it knows.
639
+ */
640
+ lastHeardAt: instant,
601
641
  /** What the agent said when the run ended, in Markdown, or `null`. */
602
642
  summary: z.nullable(z.string()),
603
643
  /** Oldest first. */
@@ -608,8 +648,8 @@ export const restRunSchema = z.object({
608
648
  });
609
649
  export type RestRun = z.infer<typeof restRunSchema>;
610
650
 
611
- /** `GET /api/v1/projects/<key>/runs`: the runs, newest first. */
612
- export const restRunsSchema = z.object({ items: z.array(restRunSchema) });
651
+ /** `GET /api/v1/projects/<key>/runs`: a page of the runs, newest first, and whether there is more. */
652
+ export const restRunsSchema = z.object({ items: z.array(restRunSchema), more: z.boolean() });
613
653
  export type RestRuns = z.infer<typeof restRunsSchema>;
614
654
 
615
655
  /** `GET .../events?after=`: what happened after that number, oldest first. */