@skillcdn/console 0.1.18 → 0.1.19

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 (121) hide show
  1. package/README.md +1 -1
  2. package/SKILLCDN.md +1 -1
  3. package/dist/api.d.ts +1 -1
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +1 -1
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts +1 -1
  8. package/dist/cli/cli.d.ts.map +1 -1
  9. package/dist/cli/cli.js +353 -39
  10. package/dist/cli/cli.js.map +1 -1
  11. package/dist/cli/credentials.d.ts +102 -3
  12. package/dist/cli/credentials.d.ts.map +1 -1
  13. package/dist/cli/credentials.js +177 -8
  14. package/dist/cli/credentials.js.map +1 -1
  15. package/dist/cli/directory.d.ts +6 -0
  16. package/dist/cli/directory.d.ts.map +1 -1
  17. package/dist/cli/directory.js +37 -6
  18. package/dist/cli/directory.js.map +1 -1
  19. package/dist/cli/format.d.ts +10 -1
  20. package/dist/cli/format.d.ts.map +1 -1
  21. package/dist/cli/format.js +106 -2
  22. package/dist/cli/format.js.map +1 -1
  23. package/dist/cli/help.d.ts.map +1 -1
  24. package/dist/cli/help.js +42 -20
  25. package/dist/cli/help.js.map +1 -1
  26. package/dist/client.d.ts +21 -1
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +14 -1
  29. package/dist/client.js.map +1 -1
  30. package/dist/components/board.d.ts +9 -1
  31. package/dist/components/board.d.ts.map +1 -1
  32. package/dist/components/board.js +82 -5
  33. package/dist/components/board.js.map +1 -1
  34. package/dist/components/comments.d.ts.map +1 -1
  35. package/dist/components/comments.js +12 -4
  36. package/dist/components/comments.js.map +1 -1
  37. package/dist/components/event-feed.d.ts.map +1 -1
  38. package/dist/components/event-feed.js +4 -0
  39. package/dist/components/event-feed.js.map +1 -1
  40. package/dist/components/goals.d.ts +17 -1
  41. package/dist/components/goals.d.ts.map +1 -1
  42. package/dist/components/goals.js +34 -9
  43. package/dist/components/goals.js.map +1 -1
  44. package/dist/components/markdown.d.ts +6 -0
  45. package/dist/components/markdown.d.ts.map +1 -1
  46. package/dist/components/markdown.js +7 -0
  47. package/dist/components/markdown.js.map +1 -1
  48. package/dist/components/pictures.d.ts +68 -0
  49. package/dist/components/pictures.d.ts.map +1 -0
  50. package/dist/components/pictures.js +213 -0
  51. package/dist/components/pictures.js.map +1 -0
  52. package/dist/components/projects.d.ts +12 -0
  53. package/dist/components/projects.d.ts.map +1 -1
  54. package/dist/components/projects.js +15 -0
  55. package/dist/components/projects.js.map +1 -1
  56. package/dist/components/task-view.d.ts +3 -0
  57. package/dist/components/task-view.d.ts.map +1 -1
  58. package/dist/components/task-view.js +6 -4
  59. package/dist/components/task-view.js.map +1 -1
  60. package/dist/console.css +282 -3
  61. package/dist/console.d.ts.map +1 -1
  62. package/dist/console.js +124 -10
  63. package/dist/console.js.map +1 -1
  64. package/dist/data.d.ts +16 -1
  65. package/dist/data.d.ts.map +1 -1
  66. package/dist/data.js +47 -2
  67. package/dist/data.js.map +1 -1
  68. package/dist/i18n/en.d.ts +52 -0
  69. package/dist/i18n/en.d.ts.map +1 -1
  70. package/dist/i18n/en.js +50 -2
  71. package/dist/i18n/en.js.map +1 -1
  72. package/dist/i18n/ko.d.ts.map +1 -1
  73. package/dist/i18n/ko.js +50 -2
  74. package/dist/i18n/ko.js.map +1 -1
  75. package/dist/index.d.ts +5 -4
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +5 -4
  78. package/dist/index.js.map +1 -1
  79. package/dist/router.d.ts +25 -6
  80. package/dist/router.d.ts.map +1 -1
  81. package/dist/router.js +38 -7
  82. package/dist/router.js.map +1 -1
  83. package/dist/rules.js +5 -5
  84. package/dist/rules.js.map +1 -1
  85. package/dist/schemas.d.ts +158 -0
  86. package/dist/schemas.d.ts.map +1 -1
  87. package/dist/schemas.js +78 -0
  88. package/dist/schemas.js.map +1 -1
  89. package/dist/vocabulary.d.ts +2 -2
  90. package/dist/vocabulary.d.ts.map +1 -1
  91. package/dist/vocabulary.js +3 -0
  92. package/dist/vocabulary.js.map +1 -1
  93. package/docs/specs/cli.md +17 -9
  94. package/docs/specs/rest.md +22 -4
  95. package/package.json +1 -1
  96. package/skills/working-the-board/SKILL.md +10 -4
  97. package/src/api.ts +13 -0
  98. package/src/cli/cli.ts +441 -42
  99. package/src/cli/credentials.ts +276 -14
  100. package/src/cli/directory.ts +43 -6
  101. package/src/cli/format.ts +138 -2
  102. package/src/cli/help.ts +44 -20
  103. package/src/client.ts +60 -0
  104. package/src/components/board.tsx +154 -8
  105. package/src/components/comments.tsx +20 -10
  106. package/src/components/event-feed.tsx +4 -0
  107. package/src/components/goals.tsx +173 -106
  108. package/src/components/markdown.tsx +19 -0
  109. package/src/components/pictures.tsx +389 -0
  110. package/src/components/projects.tsx +68 -0
  111. package/src/components/task-view.tsx +43 -2
  112. package/src/console.tsx +259 -4
  113. package/src/data.ts +72 -1
  114. package/src/i18n/en.ts +53 -2
  115. package/src/i18n/ko.ts +53 -2
  116. package/src/index.ts +27 -0
  117. package/src/router.ts +57 -9
  118. package/src/rules.ts +5 -5
  119. package/src/schemas.ts +97 -0
  120. package/src/styles/console.css +282 -3
  121. package/src/vocabulary.ts +3 -0
package/src/console.tsx CHANGED
@@ -43,11 +43,13 @@ import { ConnectWords } from "./components/connect.js";
43
43
  import { DecisionForm } from "./components/decision-form.js";
44
44
  import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
45
45
  import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
46
- import { GoalForm, GoalList, type GoalListProps, GoalView } from "./components/goals.js";
46
+ import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
47
+ import { PAGE_SIZE } from "./components/paged.js";
47
48
  import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
48
49
  import {
49
50
  MemberList,
50
51
  type MemberListProps,
52
+ PackTokenForm,
51
53
  ProjectForm,
52
54
  ProjectList,
53
55
  type ProjectListProps,
@@ -75,6 +77,8 @@ import {
75
77
  EmptyState,
76
78
  personName,
77
79
  Spinner,
80
+ StateBadge,
81
+ Time,
78
82
  } from "./components/ui.js";
79
83
  import { WorkList, type WorkListProps } from "./components/work.js";
80
84
  import { ConnectPage } from "./connect-page.js";
@@ -95,6 +99,7 @@ import {
95
99
  customPageHref,
96
100
  decisionHref,
97
101
  docHref,
102
+ type GoalTaskView,
98
103
  goalHref,
99
104
  matchRoute,
100
105
  type Navigation,
@@ -442,6 +447,102 @@ function ProjectsPage(props: {
442
447
  );
443
448
  }
444
449
 
450
+ /**
451
+ * The archive of the board (ADR-0029): the tasks put away, newest first, a page at a time from
452
+ * the server, each brought back from here by whoever may work in the project.
453
+ */
454
+ function ArchivePage(props: { readonly project: RestProject; readonly data: ConsoleData }) {
455
+ const words = useWords();
456
+ const { project, data } = props;
457
+ const key = project.key;
458
+ const { busy, error, act } = useAction();
459
+ const [items, setItems] = useState<readonly RestTask[]>([]);
460
+ const [more, setMore] = useState(false);
461
+ const [loaded, setLoaded] = useState(false);
462
+ const { archivedTasks } = data.actions;
463
+ const feedLength = data.events.length;
464
+ // The first page, read again whenever the project's feed grows: something was put away or brought back.
465
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is what triggers the read
466
+ useEffect(() => {
467
+ const controller = new AbortController();
468
+ archivedTasks({ limit: PAGE_SIZE, offset: 0 }, controller.signal)
469
+ .then((page) => {
470
+ if (!controller.signal.aborted) {
471
+ setItems(page.items);
472
+ setMore(page.more);
473
+ setLoaded(true);
474
+ }
475
+ })
476
+ .catch(() => undefined);
477
+ return () => controller.abort();
478
+ }, [archivedTasks, feedLength]);
479
+ const onMore = () =>
480
+ void archivedTasks({ limit: PAGE_SIZE, offset: items.length })
481
+ .then((page) => {
482
+ setItems((current) => [...current, ...page.items]);
483
+ setMore(page.more);
484
+ })
485
+ .catch(() => undefined);
486
+ const archive = words.archive;
487
+ return (
488
+ <>
489
+ <div className="sc-page-head">
490
+ <h1 className="sc-page-title">{archive.title}</h1>
491
+ <LinkButton href={projectHref(key)}>{words.nav.board}</LinkButton>
492
+ </div>
493
+ <p className="sc-lead">{archive.lead}</p>
494
+ {error !== undefined && <Callout tone="danger">{error}</Callout>}
495
+ {!loaded ? (
496
+ <Spinner label={archive.loading} />
497
+ ) : items.length === 0 ? (
498
+ <EmptyState title={archive.empty.title} body={archive.empty.body} />
499
+ ) : (
500
+ <>
501
+ <ul className="sc-task-rows" aria-label={archive.title}>
502
+ {items.map((task) => (
503
+ <li key={task.id} className="sc-task-row">
504
+ <a className="sc-task-row-title" href={taskHref(key, task.number)}>
505
+ <span className="sc-card-number">#{task.number}</span> {task.title}
506
+ </a>
507
+ <span className="sc-task-row-meta">
508
+ <StateBadge state={task.state} />
509
+ {task.archivedAt !== null && (
510
+ <span className="sc-muted">
511
+ {archive.since} <Time iso={task.archivedAt} />
512
+ </span>
513
+ )}
514
+ <Button
515
+ size="sm"
516
+ variant="ghost"
517
+ disabled={busy}
518
+ onClick={() =>
519
+ void act(async () => {
520
+ await data.actions.restoreTask(task.id);
521
+ setItems((current) =>
522
+ current.filter((candidate) => candidate.id !== task.id),
523
+ );
524
+ })
525
+ }
526
+ >
527
+ {archive.restore}
528
+ </Button>
529
+ </span>
530
+ </li>
531
+ ))}
532
+ </ul>
533
+ {more && (
534
+ <div className="sc-show-more">
535
+ <Button variant="secondary" size="sm" onClick={onMore}>
536
+ {archive.more}
537
+ </Button>
538
+ </div>
539
+ )}
540
+ </>
541
+ )}
542
+ </>
543
+ );
544
+ }
545
+
445
546
  function TaskPage(props: {
446
547
  readonly route: Extract<Route, { name: "task" }>;
447
548
  readonly project: RestProject;
@@ -455,10 +556,43 @@ function TaskPage(props: {
455
556
  const words = useWords();
456
557
  const { route, project, data, components, navigation, busy, error, act } = props;
457
558
  const key = project.key;
458
- const task =
559
+ const current =
459
560
  route.number === undefined
460
561
  ? data.tasks.find((candidate) => candidate.id === route.id)
461
562
  : data.tasks.find((candidate) => candidate.number === route.number);
563
+ // A task put away is not among the board's (ADR-0029): it is asked for by itself.
564
+ const [fetched, setFetched] = useState<RestTask | undefined>(undefined);
565
+ const [missing, setMissing] = useState<string | undefined>(undefined);
566
+ const { fetchTask } = data.actions;
567
+ const wanted =
568
+ current === undefined && data.projectLoaded
569
+ ? route.number === undefined
570
+ ? route.id
571
+ : String(route.number)
572
+ : undefined;
573
+ useEffect(() => {
574
+ if (wanted === undefined) {
575
+ return;
576
+ }
577
+ const controller = new AbortController();
578
+ fetchTask(wanted, controller.signal)
579
+ .then((found) => {
580
+ if (!controller.signal.aborted) {
581
+ setFetched(found);
582
+ }
583
+ })
584
+ .catch(() => {
585
+ if (!controller.signal.aborted) {
586
+ setMissing(wanted);
587
+ }
588
+ });
589
+ return () => controller.abort();
590
+ }, [wanted, fetchTask]);
591
+ const task =
592
+ current ??
593
+ (fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
594
+ ? fetched
595
+ : undefined);
462
596
  const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
463
597
  const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
464
598
  const { taskHistory, taskComments } = data.actions;
@@ -495,6 +629,9 @@ function TaskPage(props: {
495
629
  return () => controller.abort();
496
630
  }, [taskId, taskHistory, taskComments, feedLength]);
497
631
  if (task === undefined) {
632
+ if (wanted !== undefined && missing !== wanted) {
633
+ return <Spinner label={words.common.loading} />;
634
+ }
498
635
  return (
499
636
  <EmptyState
500
637
  title={words.task.noSuch.title}
@@ -545,6 +682,17 @@ function TaskPage(props: {
545
682
  void act(() => data.actions.updateDecision(decision.id, patch))
546
683
  }
547
684
  onAbandonRun={(run) => void act(() => data.actions.abandonRun(run.id))}
685
+ onArchive={(target) =>
686
+ void act(async () => {
687
+ await data.actions.archiveTask(target.id);
688
+ navigation.go(projectHref(key, "archive"));
689
+ })
690
+ }
691
+ onRestore={(target) =>
692
+ void act(async () => {
693
+ setFetched(await data.actions.restoreTask(target.id));
694
+ })
695
+ }
548
696
  onComment={(draft) =>
549
697
  void act(async () => {
550
698
  await data.actions.comment(task.id, {
@@ -615,6 +763,7 @@ function ProjectPage(props: {
615
763
  taskHref={href}
616
764
  onOpen={(task) => navigation.go(href(task))}
617
765
  onMove={onMove}
766
+ archiveHref={projectHref(key, "archive")}
618
767
  empty={
619
768
  data.tasks.length === 0 ? (
620
769
  <EmptyState
@@ -850,6 +999,10 @@ function ProjectPage(props: {
850
999
  </>
851
1000
  );
852
1001
  }
1002
+ const now = today();
1003
+ const towards = data.tasks.filter((task) => task.goalId === goal.id);
1004
+ const month = route.month ?? goalMonth(goal, now);
1005
+ const viewHref = (view: GoalTaskView) => goalHref(key, goal.id, { view });
853
1006
  return (
854
1007
  <>
855
1008
  <div className="sc-page-head">
@@ -862,13 +1015,60 @@ function ProjectPage(props: {
862
1015
  tasks={data.tasks}
863
1016
  taskHref={href}
864
1017
  docHref={toDoc}
865
- editHref={owner ? goalHref(key, goal.id, "edit") : undefined}
1018
+ today={now}
1019
+ editHref={owner ? goalHref(key, goal.id, { form: "edit" }) : undefined}
866
1020
  onMove={
867
1021
  owner
868
- ? (target, state) => void act(() => data.actions.updateGoal(target.id, { state }))
1022
+ ? (target, state) =>
1023
+ void act(async () => {
1024
+ await data.actions.updateGoal(target.id, { state });
1025
+ // A goal closed takes its done and dropped tasks to the archive, if the person says so (ADR-0029).
1026
+ const closed = towards.filter(
1027
+ (task) => !OPEN_TASK_STATES.includes(task.state),
1028
+ ).length;
1029
+ if (
1030
+ (state === "done" || state === "dropped") &&
1031
+ closed > 0 &&
1032
+ window.confirm(roadmap.archiveAsk(closed))
1033
+ ) {
1034
+ await data.actions.archiveGoalTasks(target.id);
1035
+ }
1036
+ })
1037
+ : undefined
1038
+ }
1039
+ onArchiveTasks={
1040
+ owner && towards.some((task) => !OPEN_TASK_STATES.includes(task.state))
1041
+ ? () => void act(() => data.actions.archiveGoalTasks(goal.id))
869
1042
  : undefined
870
1043
  }
871
1044
  busy={busy}
1045
+ view={route.view}
1046
+ viewHref={viewHref}
1047
+ board={
1048
+ <components.Board
1049
+ tasks={towards}
1050
+ taskHref={href}
1051
+ onOpen={(task) => navigation.go(href(task))}
1052
+ onMove={onMove}
1053
+ today={now}
1054
+ archiveHref={projectHref(key, "archive")}
1055
+ empty={
1056
+ towards.length === 0 ? <p className="sc-muted">{roadmap.noTasks}</p> : undefined
1057
+ }
1058
+ />
1059
+ }
1060
+ calendar={
1061
+ <components.Calendar
1062
+ tasks={towards}
1063
+ month={month}
1064
+ today={now}
1065
+ categories={data.categories}
1066
+ goals={[goal]}
1067
+ taskHref={href}
1068
+ goalHref={(target) => goalHref(key, target.id)}
1069
+ monthHref={(wanted) => goalHref(key, goal.id, { view: "calendar", month: wanted })}
1070
+ />
1071
+ }
872
1072
  />
873
1073
  </>
874
1074
  );
@@ -958,6 +1158,9 @@ function ProjectPage(props: {
958
1158
  </>
959
1159
  );
960
1160
  }
1161
+ if (route.name === "archive") {
1162
+ return <ArchivePage project={project} data={data} />;
1163
+ }
961
1164
  if (route.name === "feed") {
962
1165
  return (
963
1166
  <>
@@ -1156,6 +1359,39 @@ function ProjectSettingsPage(props: {
1156
1359
  </>
1157
1360
  );
1158
1361
  }
1362
+ if (route.form === "pack-token") {
1363
+ if (!owner) {
1364
+ return <NothingHere />;
1365
+ }
1366
+ return (
1367
+ <>
1368
+ {head}
1369
+ {error !== undefined && <Callout tone="danger">{error}</Callout>}
1370
+ <section className="sc-panel sc-panel-form" aria-label={words.projects.packToken.aria}>
1371
+ <PackTokenForm
1372
+ set={project.packToken !== null}
1373
+ busy={busy}
1374
+ onSubmit={(token) =>
1375
+ void act(async () => {
1376
+ await data.actions.setPackToken(token);
1377
+ navigation.replace(here);
1378
+ })
1379
+ }
1380
+ onRemove={
1381
+ project.packToken === null
1382
+ ? undefined
1383
+ : () =>
1384
+ void act(async () => {
1385
+ await data.actions.removePackToken();
1386
+ navigation.replace(here);
1387
+ })
1388
+ }
1389
+ onCancel={() => navigation.back(here)}
1390
+ />
1391
+ </section>
1392
+ </>
1393
+ );
1394
+ }
1159
1395
  return (
1160
1396
  <>
1161
1397
  {head}
@@ -1191,6 +1427,24 @@ function ProjectSettingsPage(props: {
1191
1427
  )}
1192
1428
  </dd>
1193
1429
  </div>
1430
+ <div>
1431
+ <dt>{settings.packToken}</dt>
1432
+ <dd>
1433
+ {project.packToken === null ? (
1434
+ <span className="sc-muted">{settings.packTokenNone}</span>
1435
+ ) : (
1436
+ <>
1437
+ {settings.packTokenSet} <Time iso={project.packToken.setAt} />
1438
+ </>
1439
+ )}
1440
+ {owner && (
1441
+ <>
1442
+ {" "}
1443
+ <a href={projectSettingsHref(key, "pack-token")}>{settings.packTokenEdit}</a>
1444
+ </>
1445
+ )}
1446
+ </dd>
1447
+ </div>
1194
1448
  <div>
1195
1449
  <dt>{form.symbol}</dt>
1196
1450
  <dd>
@@ -2073,6 +2327,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2073
2327
  wide={
2074
2328
  route.name === "board" ||
2075
2329
  route.name === "calendar" ||
2330
+ (route.name === "goal" && route.form === undefined) ||
2076
2331
  (route.name === "custom" &&
2077
2332
  pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
2078
2333
  }
package/src/data.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  type DocumentFilter,
7
7
  EVENT_KINDS,
8
8
  type FileUpload,
9
+ LIST_LIMIT,
10
+ type ProjectClient,
9
11
  type RestAnswerInput,
10
12
  type RestCategory,
11
13
  type RestCategoryInput,
@@ -44,6 +46,7 @@ import {
44
46
  type RestTask,
45
47
  type RestTaskInput,
46
48
  type RestTaskPatch,
49
+ type RestTasks,
47
50
  type RestToken,
48
51
  type RestTokenCreated,
49
52
  type RestTokenInput,
@@ -74,11 +77,26 @@ export interface ConsoleActions {
74
77
  /** Sets the image for the mark of the project the page is on, or takes it away; for an owner. */
75
78
  setProjectIcon(input: FileUpload): Promise<RestProject>;
76
79
  removeProjectIcon(): Promise<RestProject>;
80
+ /** Sets the pack token of the project the page is on, or takes it away; for an owner (ADR-0028). */
81
+ setPackToken(token: string): Promise<RestProject>;
82
+ removePackToken(): Promise<RestProject>;
77
83
  addMember(input: RestMemberInput): Promise<RestMember>;
78
84
  updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
79
85
  removeMember(personId: string): Promise<void>;
80
86
  createTask(input: RestTaskInput): Promise<RestTask>;
81
87
  updateTask(id: string, patch: RestTaskPatch): Promise<RestTask>;
88
+ /** One task by its id or its number, asked for by itself: a task put away is not among the board's. */
89
+ fetchTask(ref: string, signal?: AbortSignal): Promise<RestTask>;
90
+ /** Puts a done or dropped task away, or brings one back (ADR-0029). */
91
+ archiveTask(id: string): Promise<RestTask>;
92
+ restoreTask(id: string): Promise<RestTask>;
93
+ /** Puts every done or dropped task of a goal away; for an owner. Answers how many went. */
94
+ archiveGoalTasks(goalId: string): Promise<number>;
95
+ /** A page of the tasks put away, newest first. */
96
+ archivedTasks(
97
+ page: { readonly limit: number; readonly offset: number },
98
+ signal?: AbortSignal,
99
+ ): Promise<RestTasks>;
82
100
  raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
83
101
  answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
84
102
  /** Grows a decision's record: its context, or what followed. */
@@ -201,6 +219,32 @@ async function readFeed(
201
219
  return events.slice(-FEED_KEEP);
202
220
  }
203
221
 
222
+ /** How many pages of the current tasks are read before the board is shown as it stands. */
223
+ const TASKS_MAX_PAGES = 20;
224
+
225
+ /**
226
+ * The current tasks of a project, whole: page after page while the server says there is more,
227
+ * so that a board past one page loses nothing without a word; the tasks put away are not among
228
+ * them (ADR-0029).
229
+ */
230
+ async function readTasks(
231
+ scope: ProjectClient,
232
+ signal: AbortSignal | undefined,
233
+ ): Promise<{ readonly items: RestTask[] }> {
234
+ const items: RestTask[] = [];
235
+ for (let page = 0; page < TASKS_MAX_PAGES; page += 1) {
236
+ const result = await scope.tasks(
237
+ { archived: false, limit: LIST_LIMIT, offset: page * LIST_LIMIT },
238
+ signal,
239
+ );
240
+ items.push(...result.items);
241
+ if (!result.more) {
242
+ break;
243
+ }
244
+ }
245
+ return { items };
246
+ }
247
+
204
248
  const wordsOf = (failure: unknown, fallback: string): string =>
205
249
  failure instanceof ApiError ? failure.message : fallback;
206
250
 
@@ -265,7 +309,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
265
309
  const [nextProject, nextTasks, nextDecisions, nextGoals, nextRuns, nextMembers] =
266
310
  await Promise.all([
267
311
  scope.get(signal),
268
- scope.tasks(undefined, signal),
312
+ readTasks(scope, signal),
269
313
  scope.decisions(undefined, signal),
270
314
  scope.goals({}, signal),
271
315
  scope.runs(undefined, signal),
@@ -454,6 +498,16 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
454
498
  await refreshWorkspace();
455
499
  return changed;
456
500
  },
501
+ async setPackToken(token) {
502
+ const changed = await scope().setPackToken({ token });
503
+ setProject(changed);
504
+ return changed;
505
+ },
506
+ async removePackToken() {
507
+ const changed = await scope().removePackToken();
508
+ setProject(changed);
509
+ return changed;
510
+ },
457
511
  async addMember(input) {
458
512
  const member = await scope().addMember(input);
459
513
  await refreshProject();
@@ -478,6 +532,23 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
478
532
  setTasks((current) => current.map((candidate) => (candidate.id === id ? task : candidate)));
479
533
  return task;
480
534
  },
535
+ fetchTask: (ref, signal) => scope().task(ref, signal),
536
+ async archiveTask(id) {
537
+ const task = await scope().archiveTask(id);
538
+ setTasks((current) => current.filter((candidate) => candidate.id !== id));
539
+ return task;
540
+ },
541
+ async restoreTask(id) {
542
+ const task = await scope().restoreTask(id);
543
+ setTasks((current) => [task, ...current.filter((candidate) => candidate.id !== id)]);
544
+ return task;
545
+ },
546
+ async archiveGoalTasks(goalId) {
547
+ const { archived } = await scope().archiveTasksOfGoal(goalId);
548
+ await refreshProject();
549
+ return archived;
550
+ },
551
+ archivedTasks: (page, signal) => scope().tasks({ archived: true, ...page }, signal),
481
552
  async raiseDecision(input) {
482
553
  const decision = await scope().raiseDecision(input);
483
554
  await refreshProject();
package/src/i18n/en.ts CHANGED
@@ -194,12 +194,24 @@ export const en = {
194
194
  private: "The administrators own it until an owner is listed.",
195
195
  },
196
196
  },
197
+ packToken: {
198
+ aria: "The pack token",
199
+ label: "Token",
200
+ hint: "A token a person made at SkillCDN for the pack's repository, on their account page there. The console keeps it sealed, reads the pack with it for whoever may see the project, and shows it to nobody.",
201
+ placeholder: "scdn_repo_…",
202
+ set: "Set the token",
203
+ remove: "Remove the token",
204
+ },
197
205
  settings: {
198
206
  title: "Settings",
199
207
  aria: "Project settings",
200
208
  edit: "Edit the project",
201
209
  organizationsSkills: (address: string) => `The organization's: ${address}`,
202
210
  noSkills: "None: the console names no address for the organization's.",
211
+ packToken: "Pack token",
212
+ packTokenNone: "None: a public pack needs none, and a private one is not read.",
213
+ packTokenSet: "Set",
214
+ packTokenEdit: "Change it",
203
215
  },
204
216
  },
205
217
 
@@ -210,6 +222,8 @@ export const en = {
210
222
  body: "Write the first task: what is to be done, and for whom.",
211
223
  },
212
224
  moveTo: "Move to",
225
+ grip: "Drag to another column",
226
+ archive: "Archive",
213
227
  decisions: (n: number) => count(n, "decision", "decisions"),
214
228
  decisionsTitle: "Decisions waiting for a person",
215
229
  filters: {
@@ -265,6 +279,10 @@ export const en = {
265
279
  : "",
266
280
  started: "Started",
267
281
  done: "Done",
282
+ archived: "Put away",
283
+ archivedAt: "Put away",
284
+ archive: "Put away",
285
+ restore: "Bring back",
268
286
  form: {
269
287
  title: "Title",
270
288
  titlePlaceholder: "What is to be done, in plain words that stand on their own.",
@@ -394,6 +412,12 @@ export const en = {
394
412
  setBy: "Set by",
395
413
  state: "State",
396
414
  nothingMore: "Nothing more was written about it.",
415
+ about: "What reaching it means",
416
+ archiveTasks: "Put the done tasks away",
417
+ archiveAsk: (n: number) =>
418
+ `Put the ${count(n, "task", "tasks")} done or dropped under this goal away as well?`,
419
+ view: "View",
420
+ views: { board: "Board", list: "List", calendar: "Calendar" },
397
421
  timeline: {
398
422
  title: "The months",
399
423
  legend: "Category colours",
@@ -555,6 +579,8 @@ export const en = {
555
579
  taskUpdated: (fields: string, task: string) => `changed ${fields} of ${task}`,
556
580
  taskCommented: (task: string, excerpt: string | undefined) =>
557
581
  `commented on ${task}${excerpt === undefined || excerpt === "" ? "" : `: ${excerpt}`}`,
582
+ taskArchived: (task: string) => `put ${task} away`,
583
+ taskRestored: (task: string) => `brought ${task} back`,
558
584
  decisionRaised: (question: string) => `asked: ${question}`,
559
585
  decisionAnswered: (question: string, option: string) => `answered "${question}": ${option}`,
560
586
  decisionUpdated: (fields: string, question: string) =>
@@ -575,6 +601,19 @@ export const en = {
575
601
  },
576
602
  },
577
603
 
604
+ archive: {
605
+ title: "Archive",
606
+ lead: "Tasks done or dropped and put away: out of the board, the lists and the calendar, each still open by its number. Brought back from here.",
607
+ loading: "Loading the archive",
608
+ empty: {
609
+ title: "Nothing is put away",
610
+ body: "A task done or dropped is put away from its page, with its goal, or by the console after a while.",
611
+ },
612
+ since: "put away",
613
+ restore: "Bring back",
614
+ more: "Show more",
615
+ },
616
+
578
617
  runs: {
579
618
  for: "for",
580
619
  since: "since",
@@ -601,6 +640,16 @@ export const en = {
601
640
  picture: (name: string) => `The picture ${name}`,
602
641
  label: (name: string) => `A comment by ${name}`,
603
642
  },
643
+ pictures: {
644
+ open: (name: string) => `Open the picture ${name}`,
645
+ label: (name: string) => `The picture ${name}, large`,
646
+ close: "Close",
647
+ zoomIn: "Zoom in",
648
+ zoomOut: "Zoom out",
649
+ fit: "Fit to the screen",
650
+ file: "Open the file itself",
651
+ hint: "Zoom with the wheel, the buttons or two fingers; drag to move; Escape closes.",
652
+ },
604
653
 
605
654
  skills: {
606
655
  loading: "Loading the skills",
@@ -612,13 +661,15 @@ export const en = {
612
661
  servedBy: "Served by",
613
662
  at: "at",
614
663
  connect:
615
- "An agent reads these through its own MCP connection to this address, which a person makes once:",
664
+ "An agent reads these with the console command (console skill <path>), or through its own MCP connection to this address, which a person makes once:",
616
665
  connectLink: "how, on the address's page at SkillCDN",
617
666
  status: {
618
667
  indexing: "SkillCDN is still indexing the repository. Look again in a moment.",
619
668
  failed: "SkillCDN could not index the repository. What it found is on the address's page.",
620
669
  not_found:
621
- "SkillCDN does not serve this address, or serves it only to someone signed in there. The address's page says which.",
670
+ "SkillCDN does not serve this address to the console: there is nothing there, or the pack is private and the project holds no pack token for it (Settings).",
671
+ token_invalid:
672
+ "The pack token the project holds no longer works at SkillCDN: it expired, was removed, or its maker lost the repository. An owner sets a new one on the Settings page.",
622
673
  unavailable: "SkillCDN could not be reached. The skills are as they were; look again later.",
623
674
  },
624
675
  none: {