@skillcdn/console 0.1.13 → 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 (170) 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 +15 -0
  29. package/dist/components/calendar.d.ts.map +1 -1
  30. package/dist/components/calendar.js +78 -41
  31. package/dist/components/calendar.js.map +1 -1
  32. package/dist/components/connect.d.ts +4 -1
  33. package/dist/components/connect.d.ts.map +1 -1
  34. package/dist/components/connect.js +7 -3
  35. package/dist/components/connect.js.map +1 -1
  36. package/dist/components/decision-list.d.ts.map +1 -1
  37. package/dist/components/decision-list.js +3 -3
  38. package/dist/components/decision-list.js.map +1 -1
  39. package/dist/components/documents.d.ts +5 -0
  40. package/dist/components/documents.d.ts.map +1 -1
  41. package/dist/components/documents.js +3 -2
  42. package/dist/components/documents.js.map +1 -1
  43. package/dist/components/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +10 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/people.d.ts +24 -1
  48. package/dist/components/people.d.ts.map +1 -1
  49. package/dist/components/people.js +35 -3
  50. package/dist/components/people.js.map +1 -1
  51. package/dist/components/projects.d.ts +6 -0
  52. package/dist/components/projects.d.ts.map +1 -1
  53. package/dist/components/projects.js +3 -3
  54. package/dist/components/projects.js.map +1 -1
  55. package/dist/components/rules.d.ts +40 -0
  56. package/dist/components/rules.d.ts.map +1 -0
  57. package/dist/components/rules.js +46 -0
  58. package/dist/components/rules.js.map +1 -0
  59. package/dist/components/runs.d.ts +1 -1
  60. package/dist/components/runs.d.ts.map +1 -1
  61. package/dist/components/runs.js +2 -1
  62. package/dist/components/runs.js.map +1 -1
  63. package/dist/components/skills.d.ts.map +1 -1
  64. package/dist/components/skills.js +5 -1
  65. package/dist/components/skills.js.map +1 -1
  66. package/dist/components/tokens.d.ts.map +1 -1
  67. package/dist/components/tokens.js +2 -9
  68. package/dist/components/tokens.js.map +1 -1
  69. package/dist/components/ui.d.ts +7 -2
  70. package/dist/components/ui.d.ts.map +1 -1
  71. package/dist/components/ui.js +13 -0
  72. package/dist/components/ui.js.map +1 -1
  73. package/dist/components/work.d.ts +9 -1
  74. package/dist/components/work.d.ts.map +1 -1
  75. package/dist/components/work.js +8 -3
  76. package/dist/components/work.js.map +1 -1
  77. package/dist/connect-page.d.ts.map +1 -1
  78. package/dist/connect-page.js +1 -1
  79. package/dist/connect-page.js.map +1 -1
  80. package/dist/console.css +253 -13
  81. package/dist/console.d.ts.map +1 -1
  82. package/dist/console.js +203 -49
  83. package/dist/console.js.map +1 -1
  84. package/dist/data.d.ts +19 -2
  85. package/dist/data.d.ts.map +1 -1
  86. package/dist/data.js +27 -1
  87. package/dist/data.js.map +1 -1
  88. package/dist/dates.d.ts +4 -0
  89. package/dist/dates.d.ts.map +1 -1
  90. package/dist/dates.js +26 -0
  91. package/dist/dates.js.map +1 -1
  92. package/dist/i18n/en.d.ts +90 -28
  93. package/dist/i18n/en.d.ts.map +1 -1
  94. package/dist/i18n/en.js +89 -31
  95. package/dist/i18n/en.js.map +1 -1
  96. package/dist/i18n/ko.d.ts.map +1 -1
  97. package/dist/i18n/ko.js +89 -31
  98. package/dist/i18n/ko.js.map +1 -1
  99. package/dist/index.d.ts +6 -5
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +6 -5
  102. package/dist/index.js.map +1 -1
  103. package/dist/limits.d.ts +4 -0
  104. package/dist/limits.d.ts.map +1 -1
  105. package/dist/limits.js +4 -0
  106. package/dist/limits.js.map +1 -1
  107. package/dist/router.d.ts +34 -2
  108. package/dist/router.d.ts.map +1 -1
  109. package/dist/router.js +55 -7
  110. package/dist/router.js.map +1 -1
  111. package/dist/routes.d.ts +14 -4
  112. package/dist/routes.d.ts.map +1 -1
  113. package/dist/routes.js +17 -2
  114. package/dist/routes.js.map +1 -1
  115. package/dist/rules.d.ts +28 -0
  116. package/dist/rules.d.ts.map +1 -0
  117. package/dist/rules.js +102 -0
  118. package/dist/rules.js.map +1 -0
  119. package/dist/schemas.d.ts +652 -4
  120. package/dist/schemas.d.ts.map +1 -1
  121. package/dist/schemas.js +96 -6
  122. package/dist/schemas.js.map +1 -1
  123. package/dist/vocabulary.d.ts +21 -1
  124. package/dist/vocabulary.d.ts.map +1 -1
  125. package/dist/vocabulary.js +21 -0
  126. package/dist/vocabulary.js.map +1 -1
  127. package/docs/specs/cli.md +13 -9
  128. package/docs/specs/rest.md +32 -8
  129. package/package.json +1 -1
  130. package/skills/building-a-console/SKILL.md +1 -1
  131. package/skills/deploying-a-console/SKILL.md +5 -5
  132. package/skills/deploying-a-console/assets/secrets.example.json +2 -1
  133. package/skills/deploying-a-console/references/access.md +10 -6
  134. package/skills/deploying-a-console/references/resources.md +10 -7
  135. package/skills/working-the-board/SKILL.md +10 -9
  136. package/skills/working-the-board/references/project-rules.md +36 -0
  137. package/src/agent-name.ts +9 -0
  138. package/src/api.ts +37 -0
  139. package/src/cli/cli.ts +253 -24
  140. package/src/cli/format.ts +74 -10
  141. package/src/cli/help.ts +30 -10
  142. package/src/cli/main.ts +5 -0
  143. package/src/client.ts +47 -1
  144. package/src/components/calendar.tsx +242 -101
  145. package/src/components/connect.tsx +12 -6
  146. package/src/components/decision-list.tsx +2 -2
  147. package/src/components/documents.tsx +7 -3
  148. package/src/components/event-feed.tsx +16 -3
  149. package/src/components/people.tsx +244 -35
  150. package/src/components/projects.tsx +31 -8
  151. package/src/components/rules.tsx +167 -0
  152. package/src/components/runs.tsx +4 -2
  153. package/src/components/skills.tsx +22 -11
  154. package/src/components/tokens.tsx +2 -14
  155. package/src/components/ui.tsx +25 -2
  156. package/src/components/work.tsx +69 -43
  157. package/src/connect-page.tsx +0 -1
  158. package/src/console.tsx +531 -67
  159. package/src/data.ts +52 -2
  160. package/src/dates.ts +28 -0
  161. package/src/i18n/en.ts +99 -38
  162. package/src/i18n/ko.ts +95 -38
  163. package/src/index.ts +21 -1
  164. package/src/limits.ts +4 -0
  165. package/src/router.ts +77 -12
  166. package/src/routes.ts +23 -3
  167. package/src/rules.ts +109 -0
  168. package/src/schemas.ts +121 -4
  169. package/src/styles/console.css +253 -13
  170. package/src/vocabulary.ts +30 -0
package/dist/console.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { StrictMode, useCallback, useEffect, useMemo, useState, } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
- import { createClient, OPEN_TASK_STATES, } from "./api.js";
4
+ import { ApiError, createClient, OPEN_TASK_STATES, PROJECT_RULES_TEMPLATE, REFERENCE_RULES, RULES_PATH, RULES_TITLE, } from "./api.js";
5
5
  import { Board, BoardFilters, byDue, filterTasks } from "./components/board.js";
6
6
  import { Calendar, Workload } from "./components/calendar.js";
7
7
  import { CategoryList } from "./components/categories.js";
@@ -10,8 +10,9 @@ import { DecisionForm } from "./components/decision-form.js";
10
10
  import { DecisionCard, DecisionList } from "./components/decision-list.js";
11
11
  import { EventFeed } from "./components/event-feed.js";
12
12
  import { GoalForm, GoalList, GoalView } from "./components/goals.js";
13
- import { PeopleList } from "./components/people.js";
13
+ import { ListingForm, PeopleList } from "./components/people.js";
14
14
  import { MemberList, ProjectForm, ProjectList, ProjectMark, } from "./components/projects.js";
15
+ import { RulesForm, RulesView } from "./components/rules.js";
15
16
  import { isPlainClick, Shell, } from "./components/shell.js";
16
17
  import { SignIn } from "./components/sign-in.js";
17
18
  import { SkillList } from "./components/skills.js";
@@ -19,14 +20,14 @@ import { TaskForm } from "./components/task-form.js";
19
20
  import { TaskList } from "./components/task-list.js";
20
21
  import { TaskView } from "./components/task-view.js";
21
22
  import { NewToken, TokenForm, TokenList } from "./components/tokens.js";
22
- import { Button, Callout, EmptyState, personName, Spinner } from "./components/ui.js";
23
+ import { Button, Callout, CategoryBadge, EmptyState, personName, Spinner, } from "./components/ui.js";
23
24
  import { WorkList } from "./components/work.js";
24
25
  import { ConnectPage } from "./connect-page.js";
25
26
  import { useConsoleData } from "./data.js";
26
27
  import { monthOf, today } from "./dates.js";
27
28
  import { DocsPage } from "./docs-page.js";
28
29
  import { chooseLanguage, DEFAULT_LANGUAGES, LanguageContext, useWords, } from "./i18n/index.js";
29
- import { boardHref, calendarHref, customPageHref, decisionHref, docHref, goalHref, matchRoute, newDecisionHref, newGoalHref, newTaskHref, PATHS, personAgentsHref, projectHref, signInFailureOf, taskHref, withoutSignInParam, } from "./router.js";
30
+ import { boardHref, calendarHref, customPageHref, decisionHref, docHref, goalHref, matchRoute, newDecisionHref, newGoalHref, newTaskHref, PATHS, personAgentsHref, personWorkHref, projectHref, projectSettingsHref, settingsHref, signInFailureOf, taskHref, withoutSignInParam, workHref, } from "./router.js";
30
31
  import { projectPath } from "./routes.js";
31
32
  import { errorWords, useAction } from "./use-action.js";
32
33
  export const DEFAULT_COMPONENTS = {
@@ -192,7 +193,7 @@ function ProjectsPage(props) {
192
193
  const { data, components, navigation, making } = props;
193
194
  const { busy, error, act } = useAction();
194
195
  const href = (project) => projectHref(project.key);
195
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.title }), !making && (_jsx(LinkButton, { href: PATHS.newProject, variant: "primary", children: words.newProject }))] }), _jsx("p", { className: "sc-lead", children: words.lead }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.form.aria, children: _jsx(ProjectForm, { busy: busy, onSubmit: (input, icon) => void act(async () => {
196
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.title }), !making && (_jsx(LinkButton, { href: PATHS.newProject, variant: "primary", children: words.newProject }))] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.form.aria, children: _jsx(ProjectForm, { busy: busy, skillsSource: data.me?.workspace.skills.source, organizationAddress: data.me?.workspace.skills.address ?? null, onSubmit: (input, icon) => void act(async () => {
196
197
  const made = await data.actions.createProject(input, icon instanceof File ? uploadOf(icon) : undefined);
197
198
  navigation.replace(projectHref(made.key));
198
199
  }), onCancel: () => navigation.back(PATHS.projects) }) })), _jsx(components.ProjectList, { projects: data.projects, projectHref: href, onOpen: (project) => navigation.go(href(project)), empty: _jsx(EmptyState, { title: words.empty.title, body: words.empty.body, action: _jsx(LinkButton, { href: PATHS.newProject, children: words.newProject }) }) })] }));
@@ -235,7 +236,7 @@ function TaskPage(props) {
235
236
  return (_jsx(EmptyState, { title: words.task.noSuch.title, body: words.task.noSuch.body, action: _jsx(LinkButton, { href: projectHref(key), children: words.nav.board }) }));
236
237
  }
237
238
  const here = taskHref(key, task.number);
238
- return (_jsx(components.TaskView, { task: task, people: data.people, tasks: data.tasks, decisions: data.decisions, runs: data.runs, history: history, categories: data.categories, goals: data.goals, taskHref: (candidate) => taskHref(key, candidate.number), decisionHref: (decisionId) => decisionHref(key, decisionId), goalHref: (goalId) => goalHref(key, goalId), fileHref: (artifact) => projectPath(key, "files", artifact.id), docHref: (path) => docHref(key, path), form: route.form, formHref: (form) => taskHref(key, task.number, form), onCancel: () => navigation.back(here), busy: busy, error: error, onChange: (target, patch) => void act(async () => {
239
+ return (_jsx(components.TaskView, { task: task, people: data.people.filter(inWorkspace), tasks: data.tasks, decisions: data.decisions, runs: data.runs, history: history, categories: data.categories, goals: data.goals, taskHref: (candidate) => taskHref(key, candidate.number), decisionHref: (decisionId) => decisionHref(key, decisionId), goalHref: (goalId) => goalHref(key, goalId), fileHref: (artifact) => projectPath(key, "files", artifact.id), docHref: (path) => docHref(key, path), form: route.form, formHref: (form) => taskHref(key, task.number, form), onCancel: () => navigation.back(here), busy: busy, error: error, onChange: (target, patch) => void act(async () => {
239
240
  await data.actions.updateTask(target.id, patch);
240
241
  navigation.replace(here);
241
242
  }), onMove: (target, state) => void act(() => data.actions.updateTask(target.id, { state })), onRaiseDecision: (input) => void act(async () => {
@@ -258,10 +259,10 @@ function ProjectPage(props) {
258
259
  if (route.name === "board") {
259
260
  const me = data.me?.person ?? undefined;
260
261
  const shown = filterTasks(data.tasks, route.filter, me);
261
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: project.name }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] }), project.description.length > 0 && _jsx("p", { className: "sc-lead", children: project.description }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), data.tasks.length > 0 && (_jsx(BoardFilters, { filter: route.filter, categories: data.categories, goals: data.goals, people: data.people, me: me, shown: shown.length, total: data.tasks.length, onChange: (filter) => navigation.replace(boardHref(key, filter)) })), _jsx(components.Board, { tasks: shown, taskHref: href, onOpen: (task) => navigation.go(href(task)), onMove: onMove, empty: data.tasks.length === 0 ? (_jsx(EmptyState, { title: words.board.empty.title, body: words.board.empty.body, action: _jsx(LinkButton, { href: newTaskHref(key), children: words.board.writeTask }) })) : undefined })] }));
262
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: project.name }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] }), project.description.length > 0 && _jsx("p", { className: "sc-lead", children: project.description }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), data.tasks.length > 0 && (_jsx(BoardFilters, { filter: route.filter, categories: data.categories, goals: data.goals, people: data.people.filter(inWorkspace), me: me, shown: shown.length, total: data.tasks.length, onChange: (filter) => navigation.replace(boardHref(key, filter)) })), _jsx(components.Board, { tasks: shown, taskHref: href, onOpen: (task) => navigation.go(href(task)), onMove: onMove, empty: data.tasks.length === 0 ? (_jsx(EmptyState, { title: words.board.empty.title, body: words.board.empty.body, action: _jsx(LinkButton, { href: newTaskHref(key), children: words.board.writeTask }) })) : undefined })] }));
262
263
  }
263
264
  if (route.name === "new-task") {
264
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.task.newTitle }) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.task.newAria, children: _jsx(TaskForm, { people: data.people, parents: data.tasks, categories: data.categories, goals: data.goals, busy: busy, onSubmit: (input) => void act(async () => {
265
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.task.newTitle }) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.task.newAria, children: _jsx(TaskForm, { people: data.people.filter(inWorkspace), parents: data.tasks, categories: data.categories, goals: data.goals, busy: busy, onSubmit: (input) => void act(async () => {
265
266
  const made = await data.actions.createTask(input);
266
267
  navigation.replace(taskHref(key, made.number));
267
268
  }), onCancel: () => navigation.back(projectHref(key)) }) })] }));
@@ -278,7 +279,7 @@ function ProjectPage(props) {
278
279
  const overdue = open.filter((task) => task.dueOn !== null && task.dueOn < now).sort(byDue);
279
280
  const unscheduled = open.filter((task) => task.dueOn === null).sort(byDue);
280
281
  const calendar = words.calendar;
281
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: calendar.title }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] }), _jsx("p", { className: "sc-lead", children: calendar.lead }), data.tasks.length > 0 && (_jsx(BoardFilters, { filter: route.filter, categories: data.categories, goals: data.goals, people: data.people, me: me, shown: shown.length, total: data.tasks.length, onChange: (filter) => navigation.replace(calendarHref(key, route.month, filter)) })), _jsx(components.Calendar, { tasks: shown, month: month, today: now, categories: data.categories, taskHref: href, monthHref: (wanted) => calendarHref(key, wanted, route.filter) }), _jsxs("div", { className: "sc-calendar-below", children: [_jsxs("section", { className: "sc-task-section", "aria-label": calendar.overdue, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.overdue, " ", _jsx("span", { className: "sc-column-count", children: overdue.length })] }), _jsx(TaskList, { tasks: overdue, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.nothingOverdue }) })] }), _jsxs("section", { className: "sc-task-section", "aria-label": calendar.unscheduled, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.unscheduled, " ", _jsx("span", { className: "sc-column-count", children: unscheduled.length })] }), _jsx(TaskList, { tasks: unscheduled, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.allScheduled }) })] })] }), open.length > 0 && (_jsxs("section", { className: "sc-task-section", "aria-label": calendar.workload.title, children: [_jsx("h2", { className: "sc-section-title", children: calendar.workload.title }), _jsx("p", { className: "sc-lead", children: calendar.workload.lead }), _jsx(Workload, { tasks: shown, today: now, personHref: (person) => boardHref(key, person === undefined ? {} : { person: person.id }) })] }))] }));
282
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: calendar.title }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] }), data.tasks.length > 0 && (_jsx(BoardFilters, { filter: route.filter, categories: data.categories, goals: data.goals, people: data.people.filter(inWorkspace), me: me, shown: shown.length, total: data.tasks.length, onChange: (filter) => navigation.replace(calendarHref(key, route.month, filter)) })), _jsx(components.Calendar, { tasks: shown, month: month, today: now, categories: data.categories, taskHref: href, monthHref: (wanted) => calendarHref(key, wanted, route.filter) }), _jsxs("div", { className: "sc-calendar-below", children: [_jsxs("section", { className: "sc-task-section", "aria-label": calendar.overdue, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.overdue, " ", _jsx("span", { className: "sc-column-count", children: overdue.length })] }), _jsx(TaskList, { tasks: overdue, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.nothingOverdue }) })] }), _jsxs("section", { className: "sc-task-section", "aria-label": calendar.unscheduled, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.unscheduled, " ", _jsx("span", { className: "sc-column-count", children: unscheduled.length })] }), _jsx(TaskList, { tasks: unscheduled, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.allScheduled }) })] })] }), open.length > 0 && (_jsxs("section", { className: "sc-task-section", "aria-label": calendar.workload.title, children: [_jsx("h2", { className: "sc-section-title", children: calendar.workload.title }), _jsx(Workload, { tasks: shown, today: now, personHref: (person) => boardHref(key, person === undefined ? {} : { person: person.id }) })] }))] }));
282
283
  }
283
284
  if (route.name === "roadmap" || route.name === "new-goal") {
284
285
  const roadmap = words.roadmap;
@@ -286,7 +287,7 @@ function ProjectPage(props) {
286
287
  if (making && !owner) {
287
288
  return _jsx(EmptyState, { title: roadmap.onlyOwner.title, body: roadmap.onlyOwner.body });
288
289
  }
289
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: roadmap.title }), !making && owner && (_jsx(LinkButton, { href: newGoalHref(key), variant: "primary", children: roadmap.setGoal }))] }), _jsx("p", { className: "sc-lead", children: roadmap.lead }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": roadmap.setGoalAria, children: _jsx(GoalForm, { busy: busy, onSubmit: (input) => void act(async () => {
290
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: roadmap.title }), !making && owner && (_jsx(LinkButton, { href: newGoalHref(key), variant: "primary", children: roadmap.setGoal }))] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": roadmap.setGoalAria, children: _jsx(GoalForm, { busy: busy, onSubmit: (input) => void act(async () => {
290
291
  const made = await data.actions.createGoal(input);
291
292
  navigation.replace(goalHref(key, made.id));
292
293
  }), onCancel: () => navigation.back(projectHref(key, "roadmap")) }) })), _jsx(components.GoalList, { goals: data.goals, goalHref: (goal) => goalHref(key, goal.id), empty: _jsx(EmptyState, { title: roadmap.empty.title, body: roadmap.empty.body, action: owner ? (_jsx(LinkButton, { href: newGoalHref(key), children: roadmap.setGoal })) : undefined }) })] }));
@@ -339,6 +340,10 @@ function ProjectPage(props) {
339
340
  if (event.goalId !== null) {
340
341
  return goalHref(key, event.goalId);
341
342
  }
343
+ // The rules are a page shown on the settings, not among the documents (ADR-0023).
344
+ if (event.data.path === RULES_PATH) {
345
+ return projectSettingsHref(key);
346
+ }
342
347
  return event.data.path === undefined ? undefined : toDoc(event.data.path);
343
348
  }, empty: _jsx(EmptyState, { title: words.feed.empty }) })] }));
344
349
  }
@@ -349,18 +354,53 @@ function ProjectPage(props) {
349
354
  const me = data.me?.person ?? undefined;
350
355
  const configures = owner;
351
356
  const members = words.projects.members;
352
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: members.title }) }), _jsxs("p", { className: "sc-lead", children: [project.visibility === "workspace" ? members.leadWorkspace : members.leadPrivate, " ", members.leadRoles] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.MemberList, { members: data.members, people: data.people, me: me, busy: busy, onAdd: configures ? (input) => void act(() => data.actions.addMember(input)) : undefined, onChangeRole: configures
357
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: members.title }) }), _jsx("p", { className: "sc-lead", children: words.vocabulary.visibility[project.visibility] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.MemberList, { members: data.members, people: data.people.filter(inWorkspace), me: me, busy: busy, onAdd: configures ? (input) => void act(() => data.actions.addMember(input)) : undefined, onChangeRole: configures
353
358
  ? (member, role) => void act(() => data.actions.updateMember(member.person.id, { role }))
354
359
  : undefined, onRemove: configures
355
360
  ? (member) => void act(() => data.actions.removeMember(member.person.id))
356
- : undefined, empty: _jsx(EmptyState, { title: members.empty.title, body: project.visibility === "workspace" ? members.empty.workspace : members.empty.private }) })] }));
361
+ : undefined, workHref: owner ? (member) => personWorkHref(member.person.id) : undefined, empty: _jsx(EmptyState, { title: members.empty.title, body: project.visibility === "workspace" ? members.empty.workspace : members.empty.private }) })] }));
357
362
  }
358
363
  if (route.name === "settings") {
359
- const settings = words.projects.settings;
364
+ return (_jsx(ProjectSettingsPage, { route: route, project: project, data: data, navigation: navigation }));
365
+ }
366
+ return _jsx(NothingHere, {});
367
+ }
368
+ /**
369
+ * A project's settings (ADR-0023): read by every member, so that everyone sees what the project
370
+ * is and what its agents are asked; changed by an owner, on the forms at their own addresses.
371
+ */
372
+ function ProjectSettingsPage(props) {
373
+ const words = useWords();
374
+ const { route, project, data, navigation } = props;
375
+ const { busy, error, act } = useAction();
376
+ const settings = words.projects.settings;
377
+ const form = words.projects.form;
378
+ const owner = project.role === "owner";
379
+ const key = project.key;
380
+ const here = projectSettingsHref(key);
381
+ const skills = data.me?.workspace.skills;
382
+ const [rules, setRules] = useState(undefined);
383
+ const feedLength = data.events.length;
384
+ // The rules, asked for again whenever the project's feed grows.
385
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is the trigger
386
+ useEffect(() => {
387
+ const controller = new AbortController();
388
+ data.actions
389
+ .projectRules(controller.signal)
390
+ .then((found) => {
391
+ if (!controller.signal.aborted) {
392
+ setRules(found);
393
+ }
394
+ })
395
+ .catch(() => undefined);
396
+ return () => controller.abort();
397
+ }, [data.actions, feedLength]);
398
+ const head = (_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: settings.title }), owner && route.form === undefined && (_jsx(LinkButton, { href: projectSettingsHref(key, "edit"), children: words.common.edit }))] }));
399
+ if (route.form === "edit") {
360
400
  if (!owner) {
361
- return _jsx(EmptyState, { title: settings.onlyOwner.title, body: settings.onlyOwner.body });
401
+ return _jsx(NothingHere, {});
362
402
  }
363
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: settings.title }) }), _jsxs("p", { className: "sc-lead", children: [settings.leadBefore, _jsx("code", { children: project.key }), settings.leadAfter] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": settings.aria, children: _jsx(ProjectForm, { project: project, busy: busy, onSubmit: (input, icon) => void act(async () => {
403
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": settings.aria, children: _jsx(ProjectForm, { project: project, busy: busy, skillsSource: skills?.source, organizationAddress: skills?.address ?? null, onSubmit: (input, icon) => void act(async () => {
364
404
  await data.actions.updateProject(input);
365
405
  if (icon === null) {
366
406
  await data.actions.removeProjectIcon();
@@ -368,11 +408,80 @@ function ProjectPage(props) {
368
408
  else if (icon !== undefined) {
369
409
  await data.actions.setProjectIcon(uploadOf(icon));
370
410
  }
371
- }) }) })] }));
411
+ navigation.replace(here);
412
+ }), onCancel: () => navigation.back(here) }) })] }));
372
413
  }
373
- return _jsx(NothingHere, {});
414
+ if (route.form === "rules") {
415
+ if (!owner) {
416
+ return _jsx(NothingHere, {});
417
+ }
418
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.rules.projectRules, children: rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesForm, { rules: rules.project, scope: "project", template: PROJECT_RULES_TEMPLATE, busy: busy, onSubmit: (input) => void act(async () => {
419
+ await data.actions.writeDocument(RULES_PATH, {
420
+ title: RULES_TITLE,
421
+ body: input.body,
422
+ ...(input.baseVersion === undefined ? {} : { baseVersion: input.baseVersion }),
423
+ });
424
+ navigation.replace(here);
425
+ }), onCancel: () => navigation.back(here) })) })] }));
426
+ }
427
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel", "aria-label": settings.aria, children: _jsxs("dl", { className: "sc-facts sc-facts-plain", children: [_jsxs("div", { children: [_jsx("dt", { children: form.name }), _jsx("dd", { children: project.name })] }), _jsxs("div", { children: [_jsx("dt", { children: form.key }), _jsx("dd", { children: _jsx("code", { className: "sc-project-key", children: project.key }) })] }), _jsxs("div", { children: [_jsx("dt", { children: form.whoIsMember }), _jsx("dd", { children: words.vocabulary.visibility[project.visibility] })] }), _jsxs("div", { children: [_jsx("dt", { children: form.skillsAddress }), _jsx("dd", { children: project.skillsAddress !== null ? (_jsxs("code", { children: [skills?.source ?? "", project.skillsAddress] })) : skills?.address != null ? (settings.organizationsSkills(skills.address)) : (settings.noSkills) })] }), _jsxs("div", { children: [_jsx("dt", { children: form.symbol }), _jsx("dd", { children: _jsx(ProjectMark, { project: project, size: "sm" }) })] }), project.description.length > 0 && (_jsxs("div", { className: "sc-facts-wide", children: [_jsx("dt", { children: form.description }), _jsx("dd", { children: project.description })] }))] }) }), _jsxs("section", { className: "sc-panel", "aria-label": words.rules.projectRules, children: [_jsx("h2", { className: "sc-section-title", children: words.rules.projectRules }), rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesView, { rules: rules.project, scope: "project", docHref: (path) => docHref(key, path), editHref: owner ? projectSettingsHref(key, "rules") : undefined, onArchive: owner
428
+ ? () => void act(async () => {
429
+ await data.actions.archiveDocument(RULES_PATH);
430
+ setRules(await data.actions.projectRules());
431
+ })
432
+ : undefined, busy: busy }))] }), _jsxs("section", { className: "sc-panel", "aria-label": words.rules.workspaceRules, children: [_jsxs("div", { className: "sc-section-head", children: [_jsx("h2", { className: "sc-section-title", children: words.rules.workspaceRules }), _jsx("a", { className: "sc-muted", href: settingsHref(), children: words.rules.onWorkspaceSettings })] }), rules !== undefined && _jsx(RulesView, { rules: rules.workspace, scope: "workspace", folded: true })] })] }));
433
+ }
434
+ /**
435
+ * The workspace's settings (ADR-0023): its rules, the console's own until an administrator
436
+ * writes the organization's; its categories, kept on their own page; and where its skills come
437
+ * from, which whoever runs the console set. Read by everyone; the rules written by an administrator.
438
+ */
439
+ function WorkspaceSettingsPage(props) {
440
+ const words = useWords();
441
+ const { form, data, navigation } = props;
442
+ const { busy, error, act } = useAction();
443
+ const administrator = data.me?.person?.role === "admin";
444
+ const skills = data.me?.workspace.skills;
445
+ const [rules, setRules] = useState(undefined);
446
+ useEffect(() => {
447
+ const controller = new AbortController();
448
+ data.actions
449
+ .rules(controller.signal)
450
+ .then((found) => {
451
+ if (!controller.signal.aborted) {
452
+ setRules(found);
453
+ }
454
+ })
455
+ .catch(() => undefined);
456
+ return () => controller.abort();
457
+ }, [data.actions]);
458
+ const head = (_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.settings.title }) }));
459
+ if (form === "rules") {
460
+ if (!administrator) {
461
+ return _jsx(NothingHere, {});
462
+ }
463
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.rules.title, children: rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesForm, { rules: rules, scope: "workspace", template: REFERENCE_RULES, busy: busy, onSubmit: (input) => void act(async () => {
464
+ setRules(await data.actions.writeRules({
465
+ body: input.body,
466
+ ...(input.baseVersion === undefined
467
+ ? {}
468
+ : { baseVersion: input.baseVersion }),
469
+ }));
470
+ navigation.replace(PATHS.settings);
471
+ }), onCancel: () => navigation.back(PATHS.settings), onReset: () => void act(async () => {
472
+ setRules(await data.actions.archiveRules());
473
+ navigation.replace(PATHS.settings);
474
+ }) })) })] }));
475
+ }
476
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsxs("section", { className: "sc-panel", "aria-label": words.rules.title, children: [_jsx("h2", { className: "sc-section-title", children: words.rules.title }), rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesView, { rules: rules, scope: "workspace", editHref: administrator ? settingsHref("rules") : undefined, onArchive: administrator
477
+ ? () => void act(async () => {
478
+ setRules(await data.actions.archiveRules());
479
+ })
480
+ : undefined, busy: busy }))] }), _jsxs("section", { className: "sc-panel", "aria-label": words.settings.categories, children: [_jsx("h2", { className: "sc-section-title", children: words.settings.categories }), data.categories.length > 0 && (_jsx("p", { className: "sc-category-chips", children: data.categories.map((category) => (_jsx(CategoryBadge, { category: category }, category.key))) })), _jsx(LinkButton, { href: PATHS.categories, children: words.settings.manageCategories })] }), skills !== undefined && (_jsxs("section", { className: "sc-panel", "aria-label": words.settings.skills, children: [_jsx("h2", { className: "sc-section-title", children: words.settings.skills }), _jsxs("dl", { className: "sc-facts sc-facts-plain", children: [_jsxs("div", { children: [_jsx("dt", { children: words.settings.skillsSource }), _jsx("dd", { children: _jsx("a", { href: skills.source, target: "_blank", rel: "noopener noreferrer", children: skills.source }) })] }), _jsxs("div", { children: [_jsx("dt", { children: words.settings.skillsAddress }), _jsx("dd", { children: skills.address === null ? (words.settings.noSkillsAddress) : (_jsx("a", { href: `${skills.source}${skills.address}`, target: "_blank", rel: "noopener noreferrer", children: _jsx("code", { children: skills.address }) })) })] })] }), _jsx("p", { className: "sc-muted", children: words.settings.skillsFromConfiguration })] }))] }));
374
481
  }
375
482
  /** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
483
+ /** Whether a person is in: who a picker of assignees and of members offers (ADR-0022). */
484
+ const inWorkspace = (person) => person.status === "active";
376
485
  const uploadOf = (file) => ({
377
486
  name: file.name,
378
487
  bytes: file,
@@ -411,35 +520,64 @@ function PersonAgentsPage(props) {
411
520
  if (!administrator) {
412
521
  return (_jsx(EmptyState, { title: words.people.onlyAdmin.title, body: words.people.onlyAdmin.body, action: _jsx(LinkButton, { href: PATHS.agents, children: words.nav.agents }) }));
413
522
  }
414
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.people.agentsOf(person === undefined ? "?" : personName(person)) }), _jsx(LinkButton, { href: PATHS.people, children: words.nav.people })] }), _jsx("p", { className: "sc-lead", children: words.people.agentsLead(person?.name ?? person?.login ?? words.feed.someone) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), tokens === undefined ? (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.people.loadingAgents }) })) : (_jsx(components.TokenList, { tokens: tokens, busy: busy, onRevoke: (token) => void act(async () => {
523
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.people.agentsOf(person === undefined ? "?" : personName(person)) }), _jsx(LinkButton, { href: PATHS.people, children: words.nav.people })] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), tokens === undefined ? (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.people.loadingAgents }) })) : (_jsx(components.TokenList, { tokens: tokens, busy: busy, onRevoke: (token) => void act(async () => {
415
524
  await data.actions.revokePersonToken(id, token.id);
416
525
  setGeneration((current) => current + 1);
417
526
  }), empty: _jsx(EmptyState, { title: words.people.noAgent }) }))] }));
418
527
  }
419
- /** What is on the person's plate, across their projects: read when the page is opened. */
420
- function WorkPage(props) {
528
+ /**
529
+ * A plate, read when the page is opened: the person's own, or another person's for whoever
530
+ * oversees them. The week at the top is the address's.
531
+ */
532
+ function PlatePage(props) {
421
533
  const words = useWords();
422
- const { data, components } = props;
423
- const [work, setWork] = useState(undefined);
534
+ const { components, load, couldNotLoad, refused } = props;
535
+ const [plate, setPlate] = useState(undefined);
424
536
  const [problem, setProblem] = useState(undefined);
425
- const { work: load } = data.actions;
426
- const couldNotLoad = words.work.couldNotLoad;
537
+ const [forbidden, setForbidden] = useState(false);
427
538
  useEffect(() => {
428
539
  const controller = new AbortController();
540
+ setPlate(undefined);
541
+ setProblem(undefined);
542
+ setForbidden(false);
429
543
  load(controller.signal)
430
544
  .then((answer) => {
431
545
  if (!controller.signal.aborted) {
432
- setWork(answer);
546
+ setPlate(answer);
433
547
  }
434
548
  })
435
549
  .catch((failure) => {
436
- if (!controller.signal.aborted) {
550
+ if (controller.signal.aborted) {
551
+ return;
552
+ }
553
+ if (refused !== undefined &&
554
+ failure instanceof ApiError &&
555
+ failure.code === "auth.forbidden") {
556
+ setForbidden(true);
557
+ }
558
+ else {
437
559
  setProblem(errorWords(failure, couldNotLoad));
438
560
  }
439
561
  });
440
562
  return () => controller.abort();
441
- }, [load, couldNotLoad]);
442
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.work.title }) }), _jsx("p", { className: "sc-lead", children: words.work.lead }), problem !== undefined && _jsx(Callout, { tone: "danger", children: problem }), work === undefined ? (problem === undefined && (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.work.loading }) }))) : work.tasks.length === 0 && work.decisions.length === 0 ? (_jsx(EmptyState, { title: words.work.empty.title, body: words.work.empty.body })) : (_jsx(components.WorkList, { work: work, taskHref: (projectKey, task) => taskHref(projectKey, task.number), decisionHref: (projectKey, decisionId) => decisionHref(projectKey, decisionId) }))] }));
563
+ }, [load, couldNotLoad, refused]);
564
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: props.title }), props.aside] }), problem !== undefined && _jsx(Callout, { tone: "danger", children: problem }), forbidden && refused !== undefined ? (_jsx(EmptyState, { title: refused.title, body: refused.body, action: _jsx(LinkButton, { href: PATHS.work, children: words.nav.work }) })) : plate === undefined ? (problem === undefined && (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: props.loading }) }))) : plate.tasks.length === 0 && (plate.decisions?.length ?? 0) === 0 ? (_jsx(EmptyState, { title: props.empty.title, body: props.empty.body })) : (_jsx(components.WorkList, { work: plate, week: props.week, taskHref: (projectKey, task) => taskHref(projectKey, task.number), decisionHref: (projectKey, decisionId) => decisionHref(projectKey, decisionId), weekHref: props.weekHref }))] }));
565
+ }
566
+ /** What is on the person's own plate, across their projects. */
567
+ function WorkPage(props) {
568
+ const words = useWords();
569
+ const { work } = props.data.actions;
570
+ return (_jsx(PlatePage, { components: props.components, load: work, title: words.work.title, week: props.week, weekHref: workHref, loading: words.work.loading, couldNotLoad: words.work.couldNotLoad, empty: words.work.empty }));
571
+ }
572
+ /** Another person's plate, for whoever oversees them: an administrator, or an owner of a project they are in. */
573
+ function PersonWorkPage(props) {
574
+ const words = useWords();
575
+ const { id, data } = props;
576
+ const person = data.people.find((candidate) => candidate.id === id);
577
+ const name = person === undefined ? "?" : personName(person);
578
+ const { personWork } = data.actions;
579
+ const load = useCallback((signal) => personWork(id, signal), [personWork, id]);
580
+ return (_jsx(PlatePage, { components: props.components, load: load, title: words.people.workOf(name), aside: _jsx(LinkButton, { href: PATHS.people, children: words.nav.people }), week: props.week, weekHref: (day) => personWorkHref(id, day), loading: words.people.loadingWork, couldNotLoad: words.people.couldNotLoadWork, empty: { title: words.people.noWork.title(name), body: words.people.noWork.body }, refused: words.people.notOverseen }));
443
581
  }
444
582
  function WorkspacePage(props) {
445
583
  const words = useWords();
@@ -447,26 +585,42 @@ function WorkspacePage(props) {
447
585
  const [fresh, setFresh] = useState(undefined);
448
586
  const { busy, error, act } = useAction();
449
587
  if (route.name === "work") {
450
- return _jsx(WorkPage, { data: data, components: components });
588
+ return _jsx(WorkPage, { data: data, components: components, week: route.week });
589
+ }
590
+ if (route.name === "person-work") {
591
+ return _jsx(PersonWorkPage, { id: route.id, data: data, components: components, week: route.week });
592
+ }
593
+ if (route.name === "workspace-settings") {
594
+ return _jsx(WorkspaceSettingsPage, { form: route.form, data: data, navigation: navigation });
451
595
  }
452
596
  if (route.name === "categories") {
453
597
  const me = data.me?.person ?? undefined;
454
598
  const administrator = me?.role === "admin";
455
599
  const categories = words.categories;
456
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: categories.title }) }), _jsxs("p", { className: "sc-lead", children: [categories.lead, " ", categories.onlyAdmin] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.CategoryList, { categories: data.categories, busy: busy, onAdd: administrator
600
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: categories.title }) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.CategoryList, { categories: data.categories, busy: busy, onAdd: administrator
457
601
  ? (input) => void act(() => data.actions.createCategory(input))
458
602
  : undefined, onChange: administrator
459
603
  ? (category, patch) => void act(() => data.actions.updateCategory(category.key, patch))
460
604
  : undefined, onRemove: administrator
461
605
  ? (category) => void act(() => data.actions.removeCategory(category.key))
462
- : undefined, empty: _jsx(EmptyState, { title: categories.empty.title, body: categories.empty.body }) }), administrator && _jsx("p", { className: "sc-muted", children: categories.inUse })] }));
606
+ : undefined, empty: _jsx(EmptyState, { title: categories.empty.title, body: categories.empty.body }) })] }));
463
607
  }
464
- if (route.name === "people") {
608
+ if (route.name === "people" || route.name === "new-listing") {
465
609
  const me = data.me?.person ?? undefined;
466
610
  const administrator = me?.role === "admin";
467
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.people.title }) }), _jsx("p", { className: "sc-lead", children: words.people.lead }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.PeopleList, { people: data.people, me: me, busy: busy, onChangeRole: administrator
611
+ const making = route.name === "new-listing" && administrator;
612
+ // An owner sees the work of the people in their projects; the console refuses the rest.
613
+ const oversees = administrator || data.projects.some((project) => project.role === "owner");
614
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.people.title }), administrator && !making && (_jsx(LinkButton, { href: PATHS.newListing, variant: "primary", children: words.people.letIn }))] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.people.letInAria, children: _jsx(ListingForm, { providers: data.me?.signIn ?? [], busy: busy, onSubmit: (input) => void act(async () => {
615
+ await data.actions.createListing(input);
616
+ navigation.replace(PATHS.people);
617
+ }), onCancel: () => navigation.back(PATHS.people) }) })), _jsx(components.PeopleList, { people: data.people, listed: data.listed, organization: data.organization, me: me, busy: busy, onChangeRole: administrator
468
618
  ? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
469
- : undefined, agentsHref: administrator ? (person) => personAgentsHref(person.id) : undefined })] }));
619
+ : undefined, onChangeStatus: administrator
620
+ ? (person, status) => void act(() => data.actions.updatePerson(person.id, { status }))
621
+ : undefined, onUnlist: administrator
622
+ ? (listing) => void act(() => data.actions.removeListing(listing.id))
623
+ : undefined, workHref: oversees ? (person) => personWorkHref(person.id) : undefined, agentsHref: administrator ? (person) => personAgentsHref(person.id) : undefined })] }));
470
624
  }
471
625
  if (route.name === "person-agents") {
472
626
  return _jsx(PersonAgentsPage, { id: route.id, data: data, components: components });
@@ -478,7 +632,7 @@ function WorkspacePage(props) {
478
632
  const agents = words.agents;
479
633
  const making = route.name === "new-token";
480
634
  const origin = typeof window === "undefined" ? "" : window.location.origin;
481
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: agents.title }), !making && (_jsx(LinkButton, { href: PATHS.newToken, variant: "primary", children: agents.makeByHand }))] }), _jsx("p", { className: "sc-lead", children: agents.lead }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), fresh !== undefined && (_jsx(NewToken, { token: fresh.token, secret: fresh.secret, onDone: () => setFresh(undefined) })), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": agents.makeAria, children: _jsx(TokenForm, { daysAtMost: data.me?.workspace.tokenDaysAtMost, busy: busy, onSubmit: (input) => void act(async () => {
635
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: agents.title }), !making && (_jsx(LinkButton, { href: PATHS.newToken, variant: "primary", children: agents.makeByHand }))] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), fresh !== undefined && (_jsx(NewToken, { token: fresh.token, secret: fresh.secret, onDone: () => setFresh(undefined) })), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": agents.makeAria, children: _jsx(TokenForm, { daysAtMost: data.me?.workspace.tokenDaysAtMost, busy: busy, onSubmit: (input) => void act(async () => {
482
636
  setFresh(await data.actions.createToken(input));
483
637
  navigation.replace(PATHS.agents);
484
638
  }), onCancel: () => navigation.back(PATHS.agents) }) })), _jsx(components.TokenList, { tokens: data.tokens, busy: busy, onRevoke: (token) => void act(() => data.actions.revokeToken(token.id)), empty: _jsx(EmptyState, { title: agents.empty.title, body: agents.empty.body }) }), _jsx(ConnectWords, { origin: origin })] }));
@@ -536,7 +690,9 @@ export function createConsole(config = {}) {
536
690
  {
537
691
  href: PATHS.people,
538
692
  label: words.nav.people,
539
- current: route.name === "people" || route.name === "person-agents",
693
+ current: route.name === "people" ||
694
+ route.name === "new-listing" ||
695
+ route.name === "person-agents",
540
696
  },
541
697
  ...(viaToken
542
698
  ? []
@@ -547,11 +703,12 @@ export function createConsole(config = {}) {
547
703
  current: route.name === "agents" || route.name === "new-token",
548
704
  },
549
705
  ]),
550
- // The categories are read by everyone, for what the colours mean, and kept by an administrator.
706
+ // The settings are read by everyone: the rules every agent follows, the categories and
707
+ // what the colours mean, where the skills come from; kept by an administrator.
551
708
  {
552
- href: PATHS.categories,
553
- label: words.nav.categories,
554
- current: route.name === "categories",
709
+ href: PATHS.settings,
710
+ label: words.nav.settings,
711
+ current: route.name === "workspace-settings" || route.name === "categories",
555
712
  },
556
713
  ...pages
557
714
  .filter((candidate) => candidate.level === "workspace")
@@ -609,15 +766,12 @@ export function createConsole(config = {}) {
609
766
  label: words.nav.members,
610
767
  current: route.name === "members",
611
768
  },
612
- ...(project?.role === "owner"
613
- ? [
614
- {
615
- href: projectHref(projectKey, "settings"),
616
- label: words.nav.settings,
617
- current: route.name === "settings",
618
- },
619
- ]
620
- : []),
769
+ // Read by every member: what the project is, and what its agents are asked (ADR-0023).
770
+ {
771
+ href: projectHref(projectKey, "settings"),
772
+ label: words.nav.settings,
773
+ current: route.name === "settings",
774
+ },
621
775
  ...pages
622
776
  .filter((candidate) => candidate.level === "project")
623
777
  .map((candidate) => ({