@skillcdn/console 0.3.2 → 0.4.1

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 (179) hide show
  1. package/README.md +4 -3
  2. package/dist/api.d.ts +2 -2
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +132 -18
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts.map +1 -1
  10. package/dist/cli/format.js +6 -4
  11. package/dist/cli/format.js.map +1 -1
  12. package/dist/cli/help.d.ts.map +1 -1
  13. package/dist/cli/help.js +72 -21
  14. package/dist/cli/help.js.map +1 -1
  15. package/dist/cli/pack.d.ts +7 -2
  16. package/dist/cli/pack.d.ts.map +1 -1
  17. package/dist/cli/pack.js +16 -2
  18. package/dist/cli/pack.js.map +1 -1
  19. package/dist/client.d.ts +19 -3
  20. package/dist/client.d.ts.map +1 -1
  21. package/dist/client.js +11 -1
  22. package/dist/client.js.map +1 -1
  23. package/dist/compat.d.ts +1 -1
  24. package/dist/compat.js +2 -2
  25. package/dist/components/board.d.ts +12 -1
  26. package/dist/components/board.d.ts.map +1 -1
  27. package/dist/components/board.js +52 -4
  28. package/dist/components/board.js.map +1 -1
  29. package/dist/components/calendar.d.ts +4 -0
  30. package/dist/components/calendar.d.ts.map +1 -1
  31. package/dist/components/calendar.js +61 -22
  32. package/dist/components/calendar.js.map +1 -1
  33. package/dist/components/decision-form.d.ts +12 -4
  34. package/dist/components/decision-form.d.ts.map +1 -1
  35. package/dist/components/decision-form.js +24 -6
  36. package/dist/components/decision-form.js.map +1 -1
  37. package/dist/components/decision-list.d.ts +4 -0
  38. package/dist/components/decision-list.d.ts.map +1 -1
  39. package/dist/components/decision-list.js +9 -6
  40. package/dist/components/decision-list.js.map +1 -1
  41. package/dist/components/documents.d.ts.map +1 -1
  42. package/dist/components/documents.js +3 -3
  43. package/dist/components/documents.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +63 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/my-decisions.d.ts +20 -0
  51. package/dist/components/my-decisions.d.ts.map +1 -0
  52. package/dist/components/my-decisions.js +36 -0
  53. package/dist/components/my-decisions.js.map +1 -0
  54. package/dist/components/pack-connect.d.ts +3 -1
  55. package/dist/components/pack-connect.d.ts.map +1 -1
  56. package/dist/components/pack-connect.js +4 -4
  57. package/dist/components/pack-connect.js.map +1 -1
  58. package/dist/components/pack-token.d.ts +3 -1
  59. package/dist/components/pack-token.d.ts.map +1 -1
  60. package/dist/components/pack-token.js +1 -1
  61. package/dist/components/pack-token.js.map +1 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +14 -3
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/projects.d.ts.map +1 -1
  66. package/dist/components/projects.js +3 -3
  67. package/dist/components/projects.js.map +1 -1
  68. package/dist/components/remote-pictures.d.ts +6 -0
  69. package/dist/components/remote-pictures.d.ts.map +1 -0
  70. package/dist/components/remote-pictures.js +8 -0
  71. package/dist/components/remote-pictures.js.map +1 -0
  72. package/dist/components/roadmap.js +1 -1
  73. package/dist/components/roadmap.js.map +1 -1
  74. package/dist/components/runs.d.ts.map +1 -1
  75. package/dist/components/runs.js +1 -1
  76. package/dist/components/runs.js.map +1 -1
  77. package/dist/components/shell.d.ts.map +1 -1
  78. package/dist/components/shell.js +36 -1
  79. package/dist/components/shell.js.map +1 -1
  80. package/dist/components/sign-in.d.ts.map +1 -1
  81. package/dist/components/sign-in.js +49 -2
  82. package/dist/components/sign-in.js.map +1 -1
  83. package/dist/components/task-list.js +1 -1
  84. package/dist/components/task-list.js.map +1 -1
  85. package/dist/components/task-view.d.ts.map +1 -1
  86. package/dist/components/task-view.js +2 -2
  87. package/dist/components/task-view.js.map +1 -1
  88. package/dist/components/ui.d.ts +13 -2
  89. package/dist/components/ui.d.ts.map +1 -1
  90. package/dist/components/ui.js +21 -14
  91. package/dist/components/ui.js.map +1 -1
  92. package/dist/console.css +1686 -425
  93. package/dist/console.d.ts.map +1 -1
  94. package/dist/console.js +83 -13
  95. package/dist/console.js.map +1 -1
  96. package/dist/data.d.ts +15 -2
  97. package/dist/data.d.ts.map +1 -1
  98. package/dist/data.js +18 -19
  99. package/dist/data.js.map +1 -1
  100. package/dist/i18n/en.d.ts +48 -1
  101. package/dist/i18n/en.d.ts.map +1 -1
  102. package/dist/i18n/en.js +50 -3
  103. package/dist/i18n/en.js.map +1 -1
  104. package/dist/i18n/ko.d.ts.map +1 -1
  105. package/dist/i18n/ko.js +47 -4
  106. package/dist/i18n/ko.js.map +1 -1
  107. package/dist/index.d.ts +3 -2
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +3 -2
  110. package/dist/index.js.map +1 -1
  111. package/dist/limits.d.ts +14 -0
  112. package/dist/limits.d.ts.map +1 -1
  113. package/dist/limits.js +14 -0
  114. package/dist/limits.js.map +1 -1
  115. package/dist/router.d.ts +9 -1
  116. package/dist/router.d.ts.map +1 -1
  117. package/dist/router.js +12 -0
  118. package/dist/router.js.map +1 -1
  119. package/dist/routes.d.ts +5 -1
  120. package/dist/routes.d.ts.map +1 -1
  121. package/dist/routes.js +5 -1
  122. package/dist/routes.js.map +1 -1
  123. package/dist/rules.js +1 -1
  124. package/dist/rules.js.map +1 -1
  125. package/dist/schemas.d.ts +272 -4
  126. package/dist/schemas.d.ts.map +1 -1
  127. package/dist/schemas.js +33 -8
  128. package/dist/schemas.js.map +1 -1
  129. package/dist/web.d.ts.map +1 -1
  130. package/dist/web.js +3 -2
  131. package/dist/web.js.map +1 -1
  132. package/docs/specs/cli.md +11 -10
  133. package/docs/specs/rest.md +33 -17
  134. package/package.json +2 -2
  135. package/skills/building-a-console/SKILL.md +5 -5
  136. package/skills/building-a-console/references/the-default-console.md +4 -4
  137. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  138. package/skills/working-the-board/SKILL.md +6 -4
  139. package/skills/working-the-board/references/setting-up.md +1 -1
  140. package/skills/working-the-board/references/terms.md +1 -1
  141. package/src/api.ts +10 -0
  142. package/src/cli/cli.ts +162 -19
  143. package/src/cli/format.ts +11 -4
  144. package/src/cli/help.ts +79 -20
  145. package/src/cli/pack.ts +17 -1
  146. package/src/client.ts +51 -2
  147. package/src/compat.ts +2 -2
  148. package/src/components/board.tsx +114 -21
  149. package/src/components/calendar.tsx +207 -102
  150. package/src/components/decision-form.tsx +71 -23
  151. package/src/components/decision-list.tsx +30 -3
  152. package/src/components/documents.tsx +27 -11
  153. package/src/components/event-feed.tsx +116 -33
  154. package/src/components/markdown.tsx +6 -3
  155. package/src/components/my-decisions.tsx +138 -0
  156. package/src/components/pack-connect.tsx +10 -7
  157. package/src/components/pack-token.tsx +4 -1
  158. package/src/components/people.tsx +20 -8
  159. package/src/components/projects.tsx +7 -5
  160. package/src/components/remote-pictures.ts +19 -0
  161. package/src/components/roadmap.tsx +1 -1
  162. package/src/components/runs.tsx +5 -1
  163. package/src/components/shell.tsx +36 -1
  164. package/src/components/sign-in.tsx +114 -13
  165. package/src/components/task-list.tsx +1 -1
  166. package/src/components/task-view.tsx +48 -47
  167. package/src/components/ui.tsx +70 -14
  168. package/src/console.tsx +153 -32
  169. package/src/data.ts +37 -20
  170. package/src/i18n/en.ts +53 -3
  171. package/src/i18n/ko.ts +50 -4
  172. package/src/index.ts +3 -0
  173. package/src/limits.ts +14 -0
  174. package/src/router.ts +18 -1
  175. package/src/routes.ts +5 -1
  176. package/src/rules.ts +1 -1
  177. package/src/schemas.ts +37 -8
  178. package/src/styles/console.css +1686 -425
  179. package/src/web.ts +3 -2
package/src/console.tsx CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  type RestDecisionInput,
23
23
  type RestDecisionPatch,
24
24
  type RestEvent,
25
+ type RestMyDecisions,
25
26
  type RestProject,
26
27
  type RestProjectInput,
27
28
  type RestProjectPatch,
@@ -45,6 +46,7 @@ import { DecisionForm } from "./components/decision-form.js";
45
46
  import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
46
47
  import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
47
48
  import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
49
+ import { MyDecisions } from "./components/my-decisions.js";
48
50
  import { PackTokenWays } from "./components/pack-token.js";
49
51
  import { PAGE_SIZE } from "./components/paged.js";
50
52
  import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
@@ -56,6 +58,7 @@ import {
56
58
  type ProjectListProps,
57
59
  ProjectMark,
58
60
  } from "./components/projects.js";
61
+ import { type RemotePictureSrc, RemotePicturesContext } from "./components/remote-pictures.js";
59
62
  import { GoalTimeline } from "./components/roadmap.js";
60
63
  import { RulesForm, RulesView } from "./components/rules.js";
61
64
  import {
@@ -76,6 +79,7 @@ import {
76
79
  Callout,
77
80
  CategoryBadge,
78
81
  EmptyState,
82
+ LinkButton,
79
83
  personName,
80
84
  Spinner,
81
85
  StateBadge,
@@ -385,19 +389,6 @@ function NotWithToken() {
385
389
  return <EmptyState title={words.title} body={words.body} />;
386
390
  }
387
391
 
388
- /** A link that reads as a button: where a form is, for instance. */
389
- function LinkButton(props: {
390
- readonly href: string;
391
- readonly variant?: "primary" | "secondary";
392
- readonly children: React.ReactNode;
393
- }) {
394
- return (
395
- <a className={`sc-button sc-button-${props.variant ?? "secondary"}`} href={props.href}>
396
- {props.children}
397
- </a>
398
- );
399
- }
400
-
401
392
  function ProjectsPage(props: {
402
393
  readonly data: ConsoleData;
403
394
  readonly components: ConsoleComponents;
@@ -406,14 +397,18 @@ function ProjectsPage(props: {
406
397
  readonly making: boolean;
407
398
  }) {
408
399
  const words = useWords().projects;
409
- const { data, components, navigation, making } = props;
400
+ const { data, components, navigation } = props;
410
401
  const { busy, error, act } = useAction();
411
402
  const href = (project: RestProject) => projectHref(project.key);
403
+ // A project is made by an administrator (ADR-0045): a member sees the list alone, the form's
404
+ // address included.
405
+ const administrator = data.me?.person?.role === "admin";
406
+ const making = props.making && administrator;
412
407
  return (
413
408
  <>
414
409
  <div className="sc-page-head">
415
410
  <h1 className="sc-page-title">{words.title}</h1>
416
- {!making && (
411
+ {administrator && !making && (
417
412
  <LinkButton href={PATHS.newProject} variant="primary">
418
413
  {words.newProject}
419
414
  </LinkButton>
@@ -446,8 +441,12 @@ function ProjectsPage(props: {
446
441
  empty={
447
442
  <EmptyState
448
443
  title={words.empty.title}
449
- body={words.empty.body}
450
- action={<LinkButton href={PATHS.newProject}>{words.newProject}</LinkButton>}
444
+ body={administrator ? words.empty.body : words.empty.bodyMember}
445
+ action={
446
+ administrator ? (
447
+ <LinkButton href={PATHS.newProject}>{words.newProject}</LinkButton>
448
+ ) : undefined
449
+ }
451
450
  />
452
451
  }
453
452
  />
@@ -913,6 +912,19 @@ function ProjectPage(props: {
913
912
  empty={<p className="sc-muted">{calendar.nothingOverdue}</p>}
914
913
  />
915
914
  </section>
915
+ {open.length > 0 && (
916
+ <section className="sc-task-section" aria-label={calendar.workload.title}>
917
+ <h2 className="sc-section-title">{calendar.workload.title}</h2>
918
+ <Workload
919
+ tasks={shown}
920
+ today={now}
921
+ weekStart={weekStartOf(data.me?.workspace)}
922
+ personHref={(person) =>
923
+ boardHref(key, person === undefined ? {} : { person: person.id })
924
+ }
925
+ />
926
+ </section>
927
+ )}
916
928
  <section className="sc-task-section" aria-label={calendar.unscheduled}>
917
929
  <h2 className="sc-section-title">
918
930
  {calendar.unscheduled} <span className="sc-column-count">{unscheduled.length}</span>
@@ -926,19 +938,6 @@ function ProjectPage(props: {
926
938
  />
927
939
  </section>
928
940
  </div>
929
- {open.length > 0 && (
930
- <section className="sc-task-section" aria-label={calendar.workload.title}>
931
- <h2 className="sc-section-title">{calendar.workload.title}</h2>
932
- <Workload
933
- tasks={shown}
934
- today={now}
935
- weekStart={weekStartOf(data.me?.workspace)}
936
- personHref={(person) =>
937
- boardHref(key, person === undefined ? {} : { person: person.id })
938
- }
939
- />
940
- </section>
941
- )}
942
941
  </>
943
942
  );
944
943
  }
@@ -1169,7 +1168,9 @@ function ProjectPage(props: {
1169
1168
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1170
1169
  <section className="sc-panel sc-panel-form" aria-label={words.decisions.raiseAria}>
1171
1170
  <DecisionForm
1172
- tasks={data.tasks}
1171
+ findTasks={data.actions.findTasks}
1172
+ deciders={data.members.map((member) => member.person)}
1173
+ me={data.me?.person ?? undefined}
1173
1174
  busy={busy}
1174
1175
  onSubmit={(input: RestDecisionInput) =>
1175
1176
  void act(async () => {
@@ -1440,6 +1441,7 @@ function ProjectSettingsPage(props: {
1440
1441
  busy={busy}
1441
1442
  onStart={data.actions.connectPackToken}
1442
1443
  poll={data.actions.packTokenConnection}
1444
+ claim={data.actions.claimPackTokenConnection}
1443
1445
  onCancelConnect={data.actions.cancelPackTokenConnection}
1444
1446
  onConnected={() => navigation.replace(here)}
1445
1447
  onSubmit={(token) =>
@@ -1662,6 +1664,7 @@ function WorkspaceSettingsPage(props: {
1662
1664
  busy={busy}
1663
1665
  onStart={data.actions.connectWorkspacePackToken}
1664
1666
  poll={data.actions.workspacePackTokenConnection}
1667
+ claim={data.actions.claimWorkspacePackTokenConnection}
1665
1668
  onCancelConnect={data.actions.cancelWorkspacePackTokenConnection}
1666
1669
  onConnected={() => navigation.replace(PATHS.settings)}
1667
1670
  onSubmit={(token) =>
@@ -2012,6 +2015,108 @@ function PlatePage(props: {
2012
2015
  }
2013
2016
 
2014
2017
  /** What is on the person's own plate, across their projects. */
2018
+ /** How many of a person's decisions are read at a time. */
2019
+ const MY_DECISIONS_PAGE = 50;
2020
+
2021
+ /** One kind of a person's decisions, read a page at a time, as the page shows it. */
2022
+ interface DecisionsRead {
2023
+ readonly items: RestMyDecisions["items"] | undefined;
2024
+ readonly more: boolean;
2025
+ readonly busy: boolean;
2026
+ readonly problem: string | undefined;
2027
+ readonly readMore: () => void;
2028
+ /** Reads the first page again, after an answer changed what waits. */
2029
+ readonly reload: () => void;
2030
+ }
2031
+
2032
+ /** The decisions addressed to the person, or, with `raised`, those they raised for someone else (ADR-0042). */
2033
+ function useMyDecisions(
2034
+ myDecisions: ConsoleData["actions"]["myDecisions"],
2035
+ raised: boolean,
2036
+ couldNotLoad: string,
2037
+ ): DecisionsRead {
2038
+ const [items, setItems] = useState<RestMyDecisions["items"] | undefined>(undefined);
2039
+ const [more, setMore] = useState(false);
2040
+ const [busy, setBusy] = useState(false);
2041
+ const [problem, setProblem] = useState<string | undefined>(undefined);
2042
+ const read = useCallback(
2043
+ async (offset: number, signal?: AbortSignal) => {
2044
+ setBusy(true);
2045
+ try {
2046
+ const page = await myDecisions({ raised, limit: MY_DECISIONS_PAGE, offset }, signal);
2047
+ if (signal?.aborted === true) {
2048
+ return;
2049
+ }
2050
+ setItems((before) => (offset === 0 ? page.items : [...(before ?? []), ...page.items]));
2051
+ setMore(page.more);
2052
+ } catch (failure: unknown) {
2053
+ if (signal?.aborted !== true) {
2054
+ setProblem(errorWords(failure, couldNotLoad));
2055
+ }
2056
+ } finally {
2057
+ if (signal?.aborted !== true) {
2058
+ setBusy(false);
2059
+ }
2060
+ }
2061
+ },
2062
+ [myDecisions, raised, couldNotLoad],
2063
+ );
2064
+ useEffect(() => {
2065
+ const controller = new AbortController();
2066
+ setItems(undefined);
2067
+ setProblem(undefined);
2068
+ void read(0, controller.signal);
2069
+ return () => controller.abort();
2070
+ }, [read]);
2071
+ const readMore = useCallback(() => void read(items?.length ?? 0), [read, items]);
2072
+ const reload = useCallback(() => void read(0), [read]);
2073
+ return { items, more, busy, problem, readMore, reload };
2074
+ }
2075
+
2076
+ /**
2077
+ * A person's decisions (ADR-0042), laid out as a project's decisions page is: those waiting for
2078
+ * their answer, answered here, those they asked of others, and the record of both.
2079
+ */
2080
+ function MyDecisionsPage(props: { readonly data: ConsoleData }) {
2081
+ const words = useWords();
2082
+ const { myDecisions, answerDecisionIn } = props.data.actions;
2083
+ const addressed = useMyDecisions(myDecisions, false, words.myDecisions.couldNotLoad);
2084
+ const raised = useMyDecisions(myDecisions, true, words.myDecisions.couldNotLoad);
2085
+ const { busy, error, act } = useAction();
2086
+ const problem = addressed.problem ?? raised.problem ?? error;
2087
+ const onAnswer = (projectKey: string, decision: RestDecision, input: RestAnswerInput) =>
2088
+ void act(async () => {
2089
+ await answerDecisionIn(projectKey, decision.id, input);
2090
+ addressed.reload();
2091
+ raised.reload();
2092
+ });
2093
+ return (
2094
+ <>
2095
+ <div className="sc-page-head">
2096
+ <h1 className="sc-page-title">{words.myDecisions.title}</h1>
2097
+ </div>
2098
+ {problem !== undefined && <Callout tone="danger">{problem}</Callout>}
2099
+ {addressed.items === undefined || raised.items === undefined ? (
2100
+ problem === undefined && (
2101
+ <div className="sc-loading">
2102
+ <Spinner label={words.myDecisions.loading} />
2103
+ </div>
2104
+ )
2105
+ ) : addressed.items.length === 0 && raised.items.length === 0 ? (
2106
+ <EmptyState title={words.myDecisions.empty.title} body={words.myDecisions.empty.body} />
2107
+ ) : (
2108
+ <MyDecisions
2109
+ addressed={{ items: addressed.items, more: addressed.more, onMore: addressed.readMore }}
2110
+ raised={{ items: raised.items, more: raised.more, onMore: raised.readMore }}
2111
+ decisionHref={decisionHref}
2112
+ onAnswer={onAnswer}
2113
+ busy={busy || addressed.busy || raised.busy}
2114
+ />
2115
+ )}
2116
+ </>
2117
+ );
2118
+ }
2119
+
2015
2120
  function WorkPage(props: {
2016
2121
  readonly data: ConsoleData;
2017
2122
  readonly components: ConsoleComponents;
@@ -2078,6 +2183,9 @@ function WorkspacePage(props: {
2078
2183
  if (route.name === "work") {
2079
2184
  return <WorkPage data={data} components={components} week={route.week} />;
2080
2185
  }
2186
+ if (route.name === "my-decisions") {
2187
+ return <MyDecisionsPage data={data} />;
2188
+ }
2081
2189
  if (route.name === "person-work") {
2082
2190
  return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
2083
2191
  }
@@ -2237,6 +2345,8 @@ function WorkspacePage(props: {
2237
2345
  export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2238
2346
  const components: ConsoleComponents = { ...DEFAULT_COMPONENTS, ...config.components };
2239
2347
  const client = config.client ?? createClient({ baseUrl: config.baseUrl ?? "" });
2348
+ /** Where the pages read a picture from elsewhere (ADR-0048): through this client's console. */
2349
+ const pictureSrc: RemotePictureSrc = (url) => client.pictureUrl(url);
2240
2350
  const fallbackTitle = config.title ?? "Console";
2241
2351
  const languages = config.languages ?? DEFAULT_LANGUAGES;
2242
2352
  const pages = config.pages ?? [];
@@ -2321,6 +2431,11 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2321
2431
  current: route.name === "projects" || route.name === "new-project",
2322
2432
  },
2323
2433
  { href: PATHS.work, label: words.nav.work, current: route.name === "work" },
2434
+ {
2435
+ href: PATHS.decisions,
2436
+ label: words.nav.myDecisions,
2437
+ current: route.name === "my-decisions",
2438
+ },
2324
2439
  {
2325
2440
  href: PATHS.people,
2326
2441
  label: words.nav.people,
@@ -2526,7 +2641,13 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2526
2641
  </components.Shell>
2527
2642
  );
2528
2643
  }
2529
- return <LanguageContext.Provider value={pack}>{content}</LanguageContext.Provider>;
2644
+ return (
2645
+ <LanguageContext.Provider value={pack}>
2646
+ <RemotePicturesContext.Provider value={pictureSrc}>
2647
+ {content}
2648
+ </RemotePicturesContext.Provider>
2649
+ </LanguageContext.Provider>
2650
+ );
2530
2651
  }
2531
2652
 
2532
2653
  return {
package/src/data.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  type RestMemberInput,
33
33
  type RestMemberPatch,
34
34
  type RestMePatch,
35
+ type RestMyDecisions,
35
36
  type RestOrganization,
36
37
  type RestPackConnect,
37
38
  type RestPackConnectStarted,
@@ -65,12 +66,12 @@ import {
65
66
 
66
67
  // What the console shows, and how it stays current: the workspace (who is signed in, the
67
68
  // projects, the people, the tokens) is loaded once; the project the page is on is loaded whole,
68
- // its feed from the beginning, and from then on the project's stream says when something
69
+ // its feed as it stands, the newest page, and from then on the project's stream says when something
69
70
  // changed: on every event the lists are loaded again, which is the boring way to be right.
70
71
 
71
72
  /** How many events the feed keeps on the page. */
72
73
  const FEED_KEEP = 200;
73
- /** How many pages of the feed are read at the start before the stream takes over. */
74
+ /** How many pages of a task's own events are read for its thread, which is read from the beginning. */
74
75
  const FEED_MAX_PAGES = 20;
75
76
  /** How long changes that arrive together are gathered before the lists are loaded once. */
76
77
  const RELOAD_DELAY_MS = 150;
@@ -88,8 +89,10 @@ export interface ConsoleActions {
88
89
  removePackToken(): Promise<RestProject>;
89
90
  /** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
90
91
  connectPackToken(): Promise<RestPackConnectStarted>;
91
- /** How that connection stands; once connected, the project is the one answered. */
92
+ /** How that connection stands, read and changed nothing. */
92
93
  packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
94
+ /** Asks the console to claim the token at the deployment (ADR-0046); once connected, the project is the one answered. */
95
+ claimPackTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
93
96
  cancelPackTokenConnection(): Promise<void>;
94
97
  addMember(input: RestMemberInput): Promise<RestMember>;
95
98
  updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
@@ -122,6 +125,8 @@ export interface ConsoleActions {
122
125
  ): Promise<RestGoals>;
123
126
  raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
124
127
  answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
128
+ /** Answers a decision of any project the person may see, by its project's key: for the person's own page of decisions (ADR-0042). */
129
+ answerDecisionIn(projectKey: string, id: string, input: RestAnswerInput): Promise<RestDecision>;
125
130
  /** Grows a decision's record: its context, or what followed. */
126
131
  updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
127
132
  /** Sets a goal on the roadmap of the project the page is on; for an owner. */
@@ -129,6 +134,17 @@ export interface ConsoleActions {
129
134
  updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
130
135
  /** What is on the person's plate across their projects, read when the page asks. */
131
136
  work(signal?: AbortSignal): Promise<RestWork>;
137
+ /** The decisions addressed to the person (ADR-0042) across their projects, a page at a time, read when the page asks. */
138
+ myDecisions(
139
+ filter: {
140
+ readonly open?: boolean;
141
+ /** Those the person raised for someone else, in place of those addressed to them. */
142
+ readonly raised?: boolean;
143
+ readonly limit: number;
144
+ readonly offset: number;
145
+ },
146
+ signal?: AbortSignal,
147
+ ): Promise<RestMyDecisions>;
132
148
  /** The categories tasks are sorted into: kept by an administrator. */
133
149
  createCategory(input: RestCategoryInput): Promise<RestCategory>;
134
150
  updateCategory(key: string, patch: RestCategoryPatch): Promise<RestCategory>;
@@ -163,6 +179,7 @@ export interface ConsoleActions {
163
179
  removeWorkspacePackToken(): Promise<RestWorkspace>;
164
180
  connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
165
181
  workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
182
+ claimWorkspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
166
183
  cancelWorkspacePackTokenConnection(): Promise<void>;
167
184
  /** One person of the workspace by their exact login, or nobody: what an owner lists someone the People page does not show by. */
168
185
  findPerson(login: string, signal?: AbortSignal): Promise<RestPerson | undefined>;
@@ -233,23 +250,18 @@ export interface ConsoleData {
233
250
  reload(): void;
234
251
  }
235
252
 
236
- /** The whole feed of a project from the beginning, page by page, bounded. */
253
+ /**
254
+ * The feed of a project as it stands: its newest page, oldest first, from which the stream
255
+ * goes on. Read in one request, where reading it from the beginning was a round trip per
256
+ * hundred events, each waited for before the board was shown.
257
+ */
237
258
  async function readFeed(
238
259
  client: ConsoleClient,
239
260
  key: string,
240
261
  signal: AbortSignal,
241
262
  ): Promise<RestEvent[]> {
242
- const events: RestEvent[] = [];
243
- let after = 0;
244
- for (let page = 0; page < FEED_MAX_PAGES; page += 1) {
245
- const result = await client.project(key).events(after, {}, signal);
246
- events.push(...result.items);
247
- after = result.items.at(-1)?.id ?? after;
248
- if (!result.more) {
249
- break;
250
- }
251
- }
252
- return events.slice(-FEED_KEEP);
263
+ const result = await client.project(key).events(0, { latest: true }, signal);
264
+ return result.items.slice(-FEED_KEEP);
253
265
  }
254
266
 
255
267
  /** How many pages of the current tasks are read before the board is shown as it stands. */
@@ -454,7 +466,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
454
466
  return () => controller.abort();
455
467
  }, [client, generation, refreshWorkspace]);
456
468
 
457
- // The project the page is on: its lists, its feed from the beginning, and its stream from
469
+ // The project the page is on: its lists, its feed as it stands, the newest page, and its stream from
458
470
  // where the feed ends. Another project, or none, starts over.
459
471
  // biome-ignore lint/correctness/useExhaustiveDependencies: `generation` is the trigger
460
472
  useEffect(() => {
@@ -598,8 +610,9 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
598
610
  return changed;
599
611
  },
600
612
  connectPackToken: () => scope().connectPackToken(),
601
- async packTokenConnection(signal) {
602
- const state = await scope().packTokenConnection(signal);
613
+ packTokenConnection: (signal) => scope().packTokenConnection(signal),
614
+ async claimPackTokenConnection(signal) {
615
+ const state = await scope().claimPackTokenConnection(signal);
603
616
  if (state.status === "connected") {
604
617
  setProject(state.project);
605
618
  }
@@ -656,6 +669,8 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
656
669
  await refreshProject();
657
670
  return decision;
658
671
  },
672
+ answerDecisionIn: (projectKey, id, input) =>
673
+ client.project(projectKey).answerDecision(id, input),
659
674
  async answerDecision(id, input) {
660
675
  const decision = await scope().answerDecision(id, input);
661
676
  await refreshProject();
@@ -679,6 +694,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
679
694
  return goal;
680
695
  },
681
696
  work: (signal) => client.work(signal),
697
+ myDecisions: (filter, signal) => client.myDecisions(filter, signal),
682
698
  async createCategory(input) {
683
699
  const category = await client.createCategory(input);
684
700
  setCategories((await client.categories()).items);
@@ -739,8 +755,9 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
739
755
  return workspace;
740
756
  },
741
757
  connectWorkspacePackToken: () => client.connectWorkspacePackToken(),
742
- async workspacePackTokenConnection(signal) {
743
- const state = await client.workspacePackTokenConnection(signal);
758
+ workspacePackTokenConnection: (signal) => client.workspacePackTokenConnection(signal),
759
+ async claimWorkspacePackTokenConnection(signal) {
760
+ const state = await client.claimWorkspacePackTokenConnection(signal);
744
761
  if (state.status === "connected") {
745
762
  const { workspace } = state;
746
763
  setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
package/src/i18n/en.ts CHANGED
@@ -131,6 +131,7 @@ export const en = {
131
131
  nav: {
132
132
  projects: "Projects",
133
133
  work: "My work",
134
+ myDecisions: "My decisions",
134
135
  people: "People",
135
136
  agents: "Agents",
136
137
  board: "Board",
@@ -156,7 +157,15 @@ export const en = {
156
157
 
157
158
  signIn: {
158
159
  lead: "Join an organization made of AI agents.",
159
- continueWith: (provider: string) => `Continue with ${provider}`,
160
+ signInWith: (provider: string) => `Sign in with ${provider}`,
161
+ /** The button while the browser is on its way to the provider. */
162
+ going: (provider: string) => `Going to ${provider}…`,
163
+ /** The line under a button: who gets in through that way. */
164
+ admits: {
165
+ organization: (name: string) => `For the members of ${name}, the organization at GitHub.`,
166
+ domains: (domains: string) => `For accounts at ${domains}.`,
167
+ listed: "For the people an administrator added.",
168
+ },
160
169
  nobody: {
161
170
  title: "Nobody can sign in here yet",
162
171
  body: "Signing in is not configured on this console. Whoever runs it sets it up as deploy/README.md describes.",
@@ -175,6 +184,8 @@ export const en = {
175
184
  empty: {
176
185
  title: "No projects yet",
177
186
  body: "Make the first one: what it is for, and who is in it. Or ask an owner to add you to theirs.",
187
+ bodyMember:
188
+ "An administrator makes the projects. Ask one for the first, or an owner to add you to theirs.",
178
189
  },
179
190
  list: "Projects",
180
191
  private: "Private",
@@ -279,8 +290,11 @@ export const en = {
279
290
  menu: "Task menu",
280
291
  decisions: (n: number) => count(n, "decision", "decisions"),
281
292
  decisionsTitle: "Decisions waiting for a person",
293
+ unfold: (column: string) => `Open ${column}`,
294
+ fold: (column: string) => `Fold ${column}`,
282
295
  filters: {
283
296
  aria: "Narrow the board",
297
+ words: "Title, number or person",
284
298
  everyone: "Everyone",
285
299
  mine: "Mine",
286
300
  anyCategory: "Any category",
@@ -380,6 +394,8 @@ export const en = {
380
394
  record: "The record",
381
395
  openRecord: "Open",
382
396
  waitingForPerson: "Waiting for a person.",
397
+ waitingFor: (name: string) => `Waiting for ${name}.`,
398
+ peopleLabel: (asker: string, decider: string) => `asked by ${asker}, for ${decider}`,
383
399
  options: "Options",
384
400
  notePlaceholder: "A word about it, if any",
385
401
  own: "Something else",
@@ -402,6 +418,9 @@ export const en = {
402
418
  contextPlaceholder: "What a person needs to know to answer, in Markdown.",
403
419
  about: "About",
404
420
  noTask: "No task in particular",
421
+ decider: "Who decides",
422
+ deciderPlaceholder: "You, unless you name someone",
423
+ me: "(you)",
405
424
  options: "Options",
406
425
  option: (n: number) => `Option ${n}`,
407
426
  addOption: "Add an option",
@@ -409,6 +428,23 @@ export const en = {
409
428
  },
410
429
  },
411
430
 
431
+ myDecisions: {
432
+ title: "My decisions",
433
+ loading: "Loading your decisions",
434
+ couldNotLoad: "Your decisions could not be loaded.",
435
+ empty: {
436
+ title: "No decision is yours",
437
+ body: "A decision for you, by you or your agent or by someone who named you, and one you asked of someone else, show up here.",
438
+ },
439
+ waiting: "Waiting for you",
440
+ noneWaiting: "Nothing waits for your answer.",
441
+ raised: "Asked of others",
442
+ noneRaised: "You have asked nothing of others.",
443
+ answered: "Answered",
444
+ noneAnswered: "Nothing answered yet.",
445
+ more: "Show more",
446
+ },
447
+
412
448
  calendar: {
413
449
  title: "Calendar",
414
450
  grid: "The month",
@@ -421,6 +457,11 @@ export const en = {
421
457
  nothingOverdue: "Nothing is overdue.",
422
458
  unscheduled: "No day set",
423
459
  allScheduled: "Every open task has a day.",
460
+ more: (n: number) => `${n} more`,
461
+ fewer: "Fewer",
462
+ onDay: (day: string) => `Tasks on ${day}`,
463
+ nothingOnDay: "Nothing is on this day.",
464
+ close: "Close",
424
465
  week: {
425
466
  title: "The week",
426
467
  previous: "The week before",
@@ -512,8 +553,8 @@ export const en = {
512
553
  thisWeek: "Due this week",
513
554
  later: "Later",
514
555
  noDay: "No day set",
515
- decisions: "Decisions waiting for a person",
516
- noDecisions: "No decision waits.",
556
+ decisions: "Decisions waiting for you",
557
+ noDecisions: "No decision waits for your answer.",
517
558
  inProject: (project: string) => `in ${project}`,
518
559
  },
519
560
 
@@ -677,6 +718,8 @@ export const en = {
677
718
  runFinished: (task: string) => `finished ${task}`,
678
719
  runFailed: (task: string) => `failed on ${task}`,
679
720
  runAbandoned: (task: string) => `gave up on ${task}`,
721
+ runUnheard: (task: string, agent: string) =>
722
+ `ended the run of ${agent} on ${task}, not heard from for too long`,
680
723
  documentWritten: (page: string, version: number | undefined) =>
681
724
  `wrote ${page}${version === undefined ? "" : `, version ${version}`}`,
682
725
  documentArchived: (page: string) => `archived ${page}`,
@@ -801,6 +844,7 @@ export const en = {
801
844
  version: (n: number) => `v${n}`,
802
845
  versionWord: "version",
803
846
  writtenBy: "written by",
847
+ author: "Written by",
804
848
  versionOf: (n: number, of: number) => `Version ${n} of ${of}`,
805
849
  showLatest: "Show the latest",
806
850
  archivedNotice: "This page is archived: out of the folders and the search, and not written to.",
@@ -813,6 +857,8 @@ export const en = {
813
857
  decisionPrefix: "Decision: ",
814
858
  files: "Files",
815
859
  fileToAttach: "A file to attach",
860
+ chooseFile: "Choose a file",
861
+ noFileChosen: "No file chosen",
816
862
  labelPlaceholder: "What to call it, if not its name",
817
863
  attach: "Attach",
818
864
  by: "by",
@@ -853,6 +899,10 @@ export const en = {
853
899
  domain: "Of the domain",
854
900
  listed: "Added by an administrator",
855
901
  } satisfies Record<MembershipSource, string>,
902
+ /** A person the organization let in, by the organization's name: never read as their own. */
903
+ ofOrganization: (name: string) => `Member of ${name}`,
904
+ /** A person a Workspace domain let in, by the domain of their login. */
905
+ ofDomain: (domain: string) => `Account at ${domain}`,
856
906
  at: { gh: "at GitHub", google: "at Google" } satisfies Record<ProviderKey, string>,
857
907
  organization: (name: string) => `The organization ${name} at GitHub says who is in.`,
858
908
  organizationRead: "Last read",