@skillcdn/console 0.1.10 → 0.1.12

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 (145) hide show
  1. package/README.md +6 -5
  2. package/dist/api.d.ts +6 -5
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +5 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +49 -9
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +2 -0
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +7 -6
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +10 -9
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +14 -5
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +13 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/components/board.d.ts +1 -4
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +6 -13
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/calendar.d.ts +21 -1
  25. package/dist/components/calendar.d.ts.map +1 -1
  26. package/dist/components/calendar.js +69 -9
  27. package/dist/components/calendar.js.map +1 -1
  28. package/dist/components/decision-list.d.ts.map +1 -1
  29. package/dist/components/decision-list.js +11 -9
  30. package/dist/components/decision-list.js.map +1 -1
  31. package/dist/components/event-feed.d.ts.map +1 -1
  32. package/dist/components/event-feed.js +11 -7
  33. package/dist/components/event-feed.js.map +1 -1
  34. package/dist/components/paged.d.ts +14 -0
  35. package/dist/components/paged.d.ts.map +1 -0
  36. package/dist/components/paged.js +28 -0
  37. package/dist/components/paged.js.map +1 -0
  38. package/dist/components/people.js +2 -2
  39. package/dist/components/people.js.map +1 -1
  40. package/dist/components/projects.d.ts +22 -2
  41. package/dist/components/projects.d.ts.map +1 -1
  42. package/dist/components/projects.js +37 -9
  43. package/dist/components/projects.js.map +1 -1
  44. package/dist/components/runs.d.ts.map +1 -1
  45. package/dist/components/runs.js +3 -3
  46. package/dist/components/runs.js.map +1 -1
  47. package/dist/components/shell.d.ts +2 -1
  48. package/dist/components/shell.d.ts.map +1 -1
  49. package/dist/components/shell.js +3 -3
  50. package/dist/components/shell.js.map +1 -1
  51. package/dist/components/task-form.js +2 -2
  52. package/dist/components/task-form.js.map +1 -1
  53. package/dist/components/task-list.d.ts +1 -1
  54. package/dist/components/task-list.d.ts.map +1 -1
  55. package/dist/components/task-list.js +6 -4
  56. package/dist/components/task-list.js.map +1 -1
  57. package/dist/components/task-view.js +1 -1
  58. package/dist/components/task-view.js.map +1 -1
  59. package/dist/components/ui.d.ts +7 -2
  60. package/dist/components/ui.d.ts.map +1 -1
  61. package/dist/components/ui.js +9 -4
  62. package/dist/components/ui.js.map +1 -1
  63. package/dist/components/work.d.ts.map +1 -1
  64. package/dist/components/work.js +7 -3
  65. package/dist/components/work.js.map +1 -1
  66. package/dist/console.css +149 -2
  67. package/dist/console.d.ts +38 -4
  68. package/dist/console.d.ts.map +1 -1
  69. package/dist/console.js +84 -23
  70. package/dist/console.js.map +1 -1
  71. package/dist/data.d.ts +5 -2
  72. package/dist/data.d.ts.map +1 -1
  73. package/dist/data.js +15 -2
  74. package/dist/data.js.map +1 -1
  75. package/dist/i18n/en.d.ts +17 -11
  76. package/dist/i18n/en.d.ts.map +1 -1
  77. package/dist/i18n/en.js +18 -14
  78. package/dist/i18n/en.js.map +1 -1
  79. package/dist/i18n/ko.d.ts.map +1 -1
  80. package/dist/i18n/ko.js +18 -14
  81. package/dist/i18n/ko.js.map +1 -1
  82. package/dist/index.d.ts +6 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +6 -5
  85. package/dist/index.js.map +1 -1
  86. package/dist/limits.d.ts +6 -0
  87. package/dist/limits.d.ts.map +1 -1
  88. package/dist/limits.js +6 -0
  89. package/dist/limits.js.map +1 -1
  90. package/dist/marks.d.ts +12 -0
  91. package/dist/marks.d.ts.map +1 -0
  92. package/dist/marks.js +31 -0
  93. package/dist/marks.js.map +1 -0
  94. package/dist/router.d.ts +19 -3
  95. package/dist/router.d.ts.map +1 -1
  96. package/dist/router.js +28 -5
  97. package/dist/router.js.map +1 -1
  98. package/dist/routes.d.ts +7 -0
  99. package/dist/routes.d.ts.map +1 -1
  100. package/dist/routes.js +8 -0
  101. package/dist/routes.js.map +1 -1
  102. package/dist/schemas.d.ts +123 -3
  103. package/dist/schemas.d.ts.map +1 -1
  104. package/dist/schemas.js +42 -10
  105. package/dist/schemas.js.map +1 -1
  106. package/dist/vocabulary.d.ts +10 -4
  107. package/dist/vocabulary.d.ts.map +1 -1
  108. package/dist/vocabulary.js +8 -3
  109. package/dist/vocabulary.js.map +1 -1
  110. package/docs/specs/cli.md +7 -5
  111. package/docs/specs/rest.md +18 -12
  112. package/package.json +1 -1
  113. package/skills/building-a-console/SKILL.md +2 -2
  114. package/skills/working-the-board/SKILL.md +1 -1
  115. package/src/api.ts +12 -0
  116. package/src/cli/cli.ts +51 -8
  117. package/src/cli/format.ts +8 -6
  118. package/src/cli/help.ts +10 -10
  119. package/src/client.ts +29 -6
  120. package/src/components/board.tsx +6 -30
  121. package/src/components/calendar.tsx +168 -56
  122. package/src/components/decision-list.tsx +38 -33
  123. package/src/components/event-feed.tsx +34 -27
  124. package/src/components/paged.tsx +39 -0
  125. package/src/components/people.tsx +5 -5
  126. package/src/components/projects.tsx +132 -12
  127. package/src/components/runs.tsx +9 -3
  128. package/src/components/shell.tsx +8 -5
  129. package/src/components/task-form.tsx +2 -2
  130. package/src/components/task-list.tsx +35 -32
  131. package/src/components/task-view.tsx +1 -1
  132. package/src/components/ui.tsx +14 -5
  133. package/src/components/work.tsx +12 -2
  134. package/src/console.tsx +158 -25
  135. package/src/data.ts +20 -4
  136. package/src/i18n/en.ts +19 -14
  137. package/src/i18n/ko.ts +19 -14
  138. package/src/index.ts +9 -0
  139. package/src/limits.ts +6 -0
  140. package/src/marks.ts +40 -0
  141. package/src/router.ts +45 -7
  142. package/src/routes.ts +9 -0
  143. package/src/schemas.ts +48 -8
  144. package/src/styles/console.css +149 -2
  145. package/src/vocabulary.ts +14 -4
package/src/console.tsx CHANGED
@@ -1,8 +1,17 @@
1
- import { type ComponentType, StrictMode, useCallback, useEffect, useMemo, useState } from "react";
1
+ import {
2
+ type ComponentType,
3
+ type ReactNode,
4
+ StrictMode,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
2
10
  import { createRoot, type Root } from "react-dom/client";
3
11
  import {
4
12
  type ConsoleClient,
5
13
  createClient,
14
+ type FileUpload,
6
15
  OPEN_TASK_STATES,
7
16
  type RestAnswerInput,
8
17
  type RestDecision,
@@ -34,6 +43,7 @@ import {
34
43
  ProjectForm,
35
44
  ProjectList,
36
45
  type ProjectListProps,
46
+ ProjectMark,
37
47
  } from "./components/projects.js";
38
48
  import {
39
49
  type Brand,
@@ -48,7 +58,7 @@ import { TaskForm } from "./components/task-form.js";
48
58
  import { TaskList } from "./components/task-list.js";
49
59
  import { TaskView, type TaskViewProps } from "./components/task-view.js";
50
60
  import { NewToken, TokenForm, TokenList, type TokenListProps } from "./components/tokens.js";
51
- import { Button, Callout, EmptyState, Spinner } from "./components/ui.js";
61
+ import { Button, Callout, EmptyState, personName, Spinner } from "./components/ui.js";
52
62
  import { WorkList, type WorkListProps } from "./components/work.js";
53
63
  import { ConnectPage } from "./connect-page.js";
54
64
  import { type ConsoleData, useConsoleData } from "./data.js";
@@ -59,11 +69,13 @@ import {
59
69
  DEFAULT_LANGUAGES,
60
70
  LanguageContext,
61
71
  type LanguagePack,
72
+ type Messages,
62
73
  useWords,
63
74
  } from "./i18n/index.js";
64
75
  import {
65
76
  boardHref,
66
77
  calendarHref,
78
+ customPageHref,
67
79
  decisionHref,
68
80
  docHref,
69
81
  goalHref,
@@ -126,6 +138,52 @@ export const DEFAULT_COMPONENTS: ConsoleComponents = {
126
138
  CategoryList,
127
139
  };
128
140
 
141
+ /**
142
+ * A page a console adds of its own, shown as a tab beside the console's: on the workspace, or
143
+ * on every project. The address is `/pages/<key>`, or `/projects/<project>/pages/<key>`.
144
+ */
145
+ export interface CustomPage {
146
+ /** What the address says, and what tells the page apart: lowercase letters, digits and hyphens. */
147
+ readonly key: string;
148
+ /** What the tab says: one word for every language, or a word by language tag, `en` standing in for a tag without one. */
149
+ readonly label: string | Readonly<Record<string, string>>;
150
+ /** Where the tab is: among the workspace's pages, or among each project's. */
151
+ readonly level: "workspace" | "project";
152
+ /** Whether the page takes the whole width of the window, as the board does. */
153
+ readonly wide?: boolean | undefined;
154
+ /** What the page shows, given what the default pages are given. */
155
+ readonly render: (context: CustomPageContext) => ReactNode;
156
+ }
157
+
158
+ /** What a page of a console's own is given: the data the default pages read, and the means they use. */
159
+ export interface CustomPageContext {
160
+ /** The workspace and, on a project, the project the page is on, kept current; with the actions. */
161
+ readonly data: ConsoleData;
162
+ /** The project the page is on, or nothing on the workspace. */
163
+ readonly project: RestProject | undefined;
164
+ /** The client, for what the data does not carry. */
165
+ readonly client: ConsoleClient;
166
+ readonly navigation: Navigation;
167
+ /** The words of the language the pages speak, and its tag. */
168
+ readonly words: Messages;
169
+ readonly language: string;
170
+ }
171
+
172
+ /** What the tab of a page of one's own says in a language: its word for the tag, for the language alone, for English, or the first it has. */
173
+ export function customPageLabel(page: CustomPage, tag: string): string {
174
+ if (typeof page.label === "string") {
175
+ return page.label;
176
+ }
177
+ const language = tag.split("-")[0] ?? tag;
178
+ return (
179
+ page.label[tag] ??
180
+ page.label[language] ??
181
+ page.label.en ??
182
+ Object.values(page.label)[0] ??
183
+ page.key
184
+ );
185
+ }
186
+
129
187
  export interface ConsoleConfig {
130
188
  /** The origin of the console's API. Left out, the page's own origin. */
131
189
  readonly baseUrl?: string | undefined;
@@ -139,6 +197,8 @@ export interface ConsoleConfig {
139
197
  readonly brand?: Brand | undefined;
140
198
  /** The client to talk to the server with; made from `baseUrl` when left out. For tests. */
141
199
  readonly client?: ConsoleClient | undefined;
200
+ /** Pages of the console's own, as tabs beside the default ones. */
201
+ readonly pages?: readonly CustomPage[] | undefined;
142
202
  }
143
203
 
144
204
  export interface ConsoleApp {
@@ -354,9 +414,12 @@ function ProjectsPage(props: {
354
414
  <section className="sc-panel sc-panel-form" aria-label={words.form.aria}>
355
415
  <ProjectForm
356
416
  busy={busy}
357
- onSubmit={(input) =>
417
+ onSubmit={(input, icon) =>
358
418
  void act(async () => {
359
- const made = await data.actions.createProject(input as RestProjectInput);
419
+ const made = await data.actions.createProject(
420
+ input as RestProjectInput,
421
+ icon instanceof File ? uploadOf(icon) : undefined,
422
+ );
360
423
  navigation.replace(projectHref(made.key));
361
424
  })
362
425
  }
@@ -532,7 +595,6 @@ function ProjectPage(props: {
532
595
  )}
533
596
  <components.Board
534
597
  tasks={shown}
535
- runs={data.runs}
536
598
  taskHref={href}
537
599
  onOpen={(task) => navigation.go(href(task))}
538
600
  onMove={onMove}
@@ -592,7 +654,9 @@ function ProjectPage(props: {
592
654
  if (route.name === "calendar") {
593
655
  const now = today();
594
656
  const month = route.month ?? monthOf(now);
595
- const open = data.tasks.filter((task) => OPEN_TASK_STATES.includes(task.state));
657
+ const me = data.me?.person ?? undefined;
658
+ const shown = filterTasks(data.tasks, route.filter, me);
659
+ const open = shown.filter((task) => OPEN_TASK_STATES.includes(task.state));
596
660
  const overdue = open.filter((task) => task.dueOn !== null && task.dueOn < now).sort(byDue);
597
661
  const unscheduled = open.filter((task) => task.dueOn === null).sort(byDue);
598
662
  const calendar = words.calendar;
@@ -605,12 +669,25 @@ function ProjectPage(props: {
605
669
  </LinkButton>
606
670
  </div>
607
671
  <p className="sc-lead">{calendar.lead}</p>
672
+ {data.tasks.length > 0 && (
673
+ <BoardFilters
674
+ filter={route.filter}
675
+ categories={data.categories}
676
+ goals={data.goals}
677
+ people={data.people}
678
+ me={me}
679
+ shown={shown.length}
680
+ total={data.tasks.length}
681
+ onChange={(filter) => navigation.replace(calendarHref(key, route.month, filter))}
682
+ />
683
+ )}
608
684
  <components.Calendar
609
- tasks={data.tasks}
685
+ tasks={shown}
610
686
  month={month}
611
687
  today={now}
688
+ categories={data.categories}
612
689
  taskHref={href}
613
- monthHref={(wanted) => calendarHref(key, wanted)}
690
+ monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
614
691
  />
615
692
  <div className="sc-calendar-below">
616
693
  <section className="sc-task-section" aria-label={calendar.overdue}>
@@ -643,7 +720,7 @@ function ProjectPage(props: {
643
720
  <h2 className="sc-section-title">{calendar.workload.title}</h2>
644
721
  <p className="sc-lead">{calendar.workload.lead}</p>
645
722
  <Workload
646
- tasks={data.tasks}
723
+ tasks={shown}
647
724
  today={now}
648
725
  personHref={(person) =>
649
726
  boardHref(key, person === undefined ? {} : { person: person.id })
@@ -950,8 +1027,15 @@ function ProjectPage(props: {
950
1027
  <ProjectForm
951
1028
  project={project}
952
1029
  busy={busy}
953
- onSubmit={(input) =>
954
- void act(() => data.actions.updateProject(input as RestProjectPatch))
1030
+ onSubmit={(input, icon) =>
1031
+ void act(async () => {
1032
+ await data.actions.updateProject(input as RestProjectPatch);
1033
+ if (icon === null) {
1034
+ await data.actions.removeProjectIcon();
1035
+ } else if (icon !== undefined) {
1036
+ await data.actions.setProjectIcon(uploadOf(icon));
1037
+ }
1038
+ })
955
1039
  }
956
1040
  />
957
1041
  </section>
@@ -961,6 +1045,13 @@ function ProjectPage(props: {
961
1045
  return <NothingHere />;
962
1046
  }
963
1047
 
1048
+ /** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
1049
+ const uploadOf = (file: File): FileUpload => ({
1050
+ name: file.name,
1051
+ bytes: file,
1052
+ ...(file.type === "" ? {} : { contentType: file.type }),
1053
+ });
1054
+
964
1055
  function NothingHere() {
965
1056
  const words = useWords();
966
1057
  return (
@@ -1014,7 +1105,9 @@ function PersonAgentsPage(props: {
1014
1105
  return (
1015
1106
  <>
1016
1107
  <div className="sc-page-head">
1017
- <h1 className="sc-page-title">{words.people.agentsOf(person?.login ?? "?")}</h1>
1108
+ <h1 className="sc-page-title">
1109
+ {words.people.agentsOf(person === undefined ? "?" : personName(person))}
1110
+ </h1>
1018
1111
  <LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>
1019
1112
  </div>
1020
1113
  <p className="sc-lead">
@@ -1226,6 +1319,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
1226
1319
  const client = config.client ?? createClient({ baseUrl: config.baseUrl ?? "" });
1227
1320
  const fallbackTitle = config.title ?? "Console";
1228
1321
  const languages = config.languages ?? DEFAULT_LANGUAGES;
1322
+ const pages = config.pages ?? [];
1229
1323
 
1230
1324
  function App(props: { readonly initialPath?: string | undefined }) {
1231
1325
  const { location, navigation } = useNavigation(props.initialPath);
@@ -1324,16 +1418,22 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
1324
1418
  current: route.name === "agents" || route.name === "new-token",
1325
1419
  },
1326
1420
  ]),
1327
- // The categories are read by everyone and kept by an administrator; only they need the page.
1328
- ...(data.me.person.role === "admin" && !viaToken
1329
- ? [
1330
- {
1331
- href: PATHS.categories,
1332
- label: words.nav.categories,
1333
- current: route.name === "categories",
1334
- },
1335
- ]
1336
- : []),
1421
+ // The categories are read by everyone, for what the colours mean, and kept by an administrator.
1422
+ {
1423
+ href: PATHS.categories,
1424
+ label: words.nav.categories,
1425
+ current: route.name === "categories",
1426
+ },
1427
+ ...pages
1428
+ .filter((candidate) => candidate.level === "workspace")
1429
+ .map((candidate) => ({
1430
+ href: customPageHref(candidate.key),
1431
+ label: customPageLabel(candidate, pack.tag),
1432
+ current:
1433
+ route.name === "custom" &&
1434
+ route.level === "workspace" &&
1435
+ route.page === candidate.key,
1436
+ })),
1337
1437
  ];
1338
1438
  const nav: NavItem[] =
1339
1439
  projectKey === undefined
@@ -1394,8 +1494,27 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
1394
1494
  },
1395
1495
  ]
1396
1496
  : []),
1497
+ ...pages
1498
+ .filter((candidate) => candidate.level === "project")
1499
+ .map((candidate) => ({
1500
+ href: customPageHref(candidate.key, projectKey),
1501
+ label: customPageLabel(candidate, pack.tag),
1502
+ current:
1503
+ route.name === "custom" &&
1504
+ route.level === "project" &&
1505
+ route.page === candidate.key,
1506
+ })),
1397
1507
  ];
1398
- let page: React.ReactNode;
1508
+ /** A page of the console's own at the address, or nothing there. */
1509
+ const customPageOf = (key: string, level: CustomPage["level"]): ReactNode => {
1510
+ const found = pages.find((candidate) => candidate.key === key && candidate.level === level);
1511
+ return found === undefined ? (
1512
+ <NothingHere />
1513
+ ) : (
1514
+ found.render({ data, project, client, navigation, words, language: pack.tag })
1515
+ );
1516
+ };
1517
+ let page: ReactNode;
1399
1518
  if (!data.loaded || route.name === "moved") {
1400
1519
  page = (
1401
1520
  <div className="sc-loading">
@@ -1413,6 +1532,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
1413
1532
  making={route.name === "new-project"}
1414
1533
  />
1415
1534
  );
1535
+ } else if (route.name === "custom" && route.level === "workspace") {
1536
+ page = customPageOf(route.page, "workspace");
1416
1537
  } else if (projectKey === undefined) {
1417
1538
  page = (
1418
1539
  <WorkspacePage
@@ -1436,6 +1557,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
1436
1557
  <Spinner label={words.common.loadingProject} />
1437
1558
  </div>
1438
1559
  );
1560
+ } else if (route.name === "custom") {
1561
+ page = customPageOf(route.page, "project");
1439
1562
  } else {
1440
1563
  page = (
1441
1564
  <ProjectPage
@@ -1454,7 +1577,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
1454
1577
  project={
1455
1578
  projectKey === undefined
1456
1579
  ? undefined
1457
- : { name: project?.name ?? projectKey, href: projectHref(projectKey) }
1580
+ : {
1581
+ name: project?.name ?? projectKey,
1582
+ href: projectHref(projectKey),
1583
+ mark:
1584
+ project === undefined ? undefined : <ProjectMark project={project} size="sm" />,
1585
+ }
1458
1586
  }
1459
1587
  nav={nav}
1460
1588
  menu={workspaceNav}
@@ -1473,7 +1601,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
1473
1601
  }}
1474
1602
  person={data.me.person}
1475
1603
  live={projectKey === undefined ? undefined : data.live}
1476
- wide={route.name === "board" || route.name === "calendar"}
1604
+ wide={
1605
+ route.name === "board" ||
1606
+ route.name === "calendar" ||
1607
+ (route.name === "custom" &&
1608
+ pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
1609
+ }
1477
1610
  onNavigate={(href) => navigation.go(href)}
1478
1611
  onSignOut={viaToken ? undefined : () => void data.actions.signOut()}
1479
1612
  >
package/src/data.ts CHANGED
@@ -58,10 +58,13 @@ const FEED_MAX_PAGES = 20;
58
58
  const RELOAD_DELAY_MS = 150;
59
59
 
60
60
  export interface ConsoleActions {
61
- /** Makes a project, whose owner the person becomes. */
62
- createProject(input: RestProjectInput): Promise<RestProject>;
61
+ /** Makes a project, whose owner the person becomes, with an image for its mark when one is given. */
62
+ createProject(input: RestProjectInput, icon?: FileUpload): Promise<RestProject>;
63
63
  /** Changes the settings of the project the page is on; for an owner. */
64
64
  updateProject(patch: RestProjectPatch): Promise<RestProject>;
65
+ /** Sets the image for the mark of the project the page is on, or takes it away; for an owner. */
66
+ setProjectIcon(input: FileUpload): Promise<RestProject>;
67
+ removeProjectIcon(): Promise<RestProject>;
65
68
  addMember(input: RestMemberInput): Promise<RestMember>;
66
69
  updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
67
70
  removeMember(personId: string): Promise<void>;
@@ -387,10 +390,11 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
387
390
  return client.project(projectKey);
388
391
  };
389
392
  return {
390
- async createProject(input) {
393
+ async createProject(input, icon) {
391
394
  const made = await client.createProject(input);
395
+ const marked = icon === undefined ? made : await client.project(made.key).setIcon(icon);
392
396
  await refreshWorkspace();
393
- return made;
397
+ return marked;
394
398
  },
395
399
  async updateProject(patch) {
396
400
  const changed = await scope().update(patch);
@@ -398,6 +402,18 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
398
402
  await refreshWorkspace();
399
403
  return changed;
400
404
  },
405
+ async setProjectIcon(input) {
406
+ const changed = await scope().setIcon(input);
407
+ setProject(changed);
408
+ await refreshWorkspace();
409
+ return changed;
410
+ },
411
+ async removeProjectIcon() {
412
+ const changed = await scope().removeIcon();
413
+ setProject(changed);
414
+ await refreshWorkspace();
415
+ return changed;
416
+ },
401
417
  async addMember(input) {
402
418
  const member = await scope().addMember(input);
403
419
  await refreshProject();
package/src/i18n/en.ts CHANGED
@@ -43,7 +43,7 @@ export const en = {
43
43
  private: "Only those listed",
44
44
  } satisfies Record<ProjectVisibility, string>,
45
45
  runStatus: {
46
- running: "At work",
46
+ running: "Open",
47
47
  waiting: "Waiting for a decision",
48
48
  finished: "Finished",
49
49
  failed: "Failed",
@@ -95,6 +95,7 @@ export const en = {
95
95
  nobody: "Nobody",
96
96
  nothing: "Nothing",
97
97
  language: "Language",
98
+ showMore: (n: number, of: number) => `Show ${n} more of ${of}`,
98
99
  },
99
100
 
100
101
  nav: {
@@ -121,7 +122,7 @@ export const en = {
121
122
  live: "Live",
122
123
  reconnecting: "Reconnecting",
123
124
  signOut: "Sign out",
124
- menu: (login: string) => `${login}: menu`,
125
+ menu: (name: string) => `${name}: menu`,
125
126
  },
126
127
 
127
128
  signIn: {
@@ -150,7 +151,6 @@ export const en = {
150
151
  list: "Projects",
151
152
  private: "Private",
152
153
  decisionsWaiting: (n: number) => `${count(n, "decision", "decisions")} waiting`,
153
- agentsAtWork: (n: number) => `${count(n, "agent", "agents")} at work`,
154
154
  form: {
155
155
  aria: "New project",
156
156
  name: "Name",
@@ -162,6 +162,11 @@ export const en = {
162
162
  whoIsMember: "Who is a member",
163
163
  skillsAddress: "Skills address",
164
164
  skillsAddressPlaceholder: "/gh/<owner>/<repo>, or empty for the organization's",
165
+ symbol: "Mark",
166
+ image: "Image",
167
+ imageRemove: "Remove the image",
168
+ color: "Colour",
169
+ colorAuto: "Chosen from the key",
165
170
  make: "Make the project",
166
171
  },
167
172
  members: {
@@ -173,7 +178,7 @@ export const en = {
173
178
  leadRoles:
174
179
  "An owner configures the project; a member works on it. A workspace administrator is an owner of every project.",
175
180
  since: "since",
176
- roleOf: (login: string) => `Role of ${login}`,
181
+ roleOf: (name: string) => `Role of ${name}`,
177
182
  addAria: "Add a member",
178
183
  person: "Person",
179
184
  chooseSomeone: "Choose someone",
@@ -207,9 +212,6 @@ export const en = {
207
212
  moveTo: "Move to",
208
213
  decisions: (n: number) => count(n, "decision", "decisions"),
209
214
  decisionsTitle: "Decisions waiting for a person",
210
- agentAtWork: "agent at work",
211
- agentAtWorkTitle: "An agent is at work on it",
212
- agentSince: (agent: string, since: string) => `${agent}, at work since ${since}`,
213
215
  filters: {
214
216
  aria: "Narrow the board",
215
217
  everyone: "Everyone",
@@ -247,7 +249,6 @@ export const en = {
247
249
  raiseDecision: "Raise a decision",
248
250
  nothingMore: "Nothing more was written about it.",
249
251
  links: "Links",
250
- agentsAtWork: "Agents at work",
251
252
  runs: "Runs",
252
253
  decisions: "Decisions",
253
254
  history: "What happened",
@@ -336,8 +337,10 @@ export const en = {
336
337
 
337
338
  calendar: {
338
339
  title: "Calendar",
339
- lead: "The tasks by the day they are due. A task with a start is shown from it; one with no day waits at the bottom to be given one.",
340
+ lead: "The tasks on their days: planned from a start to a due day, a task lies across them; due on a day alone, it sits on it. One with no day waits at the bottom to be given one.",
340
341
  grid: "The month",
342
+ legend: { title: "Category colours" },
343
+ startsOnly: "Starts that day, with no due day yet",
341
344
  previous: "The month before",
342
345
  next: "The month after",
343
346
  thisMonth: "This month",
@@ -346,7 +349,6 @@ export const en = {
346
349
  nothingOverdue: "Nothing is overdue.",
347
350
  unscheduled: "No day set",
348
351
  allScheduled: "Every open task has a day.",
349
- starts: "starts",
350
352
  workload: {
351
353
  title: "Who has what",
352
354
  lead: "The open tasks of each person, and what of them is late or due within the week.",
@@ -509,6 +511,7 @@ export const en = {
509
511
  runReported: (task: string, excerpt: string | undefined) =>
510
512
  `reported on ${task}${excerpt === undefined ? "" : `: ${excerpt}`}`,
511
513
  runHandedIn: (label: string, task: string) => `handed in ${label} on ${task}`,
514
+ runTakenOver: (task: string, agent: string) => `took over ${task} from ${agent}`,
512
515
  runFinished: (task: string) => `finished ${task}`,
513
516
  runFailed: (task: string) => `failed on ${task}`,
514
517
  runAbandoned: (task: string) => `gave up on ${task}`,
@@ -524,12 +527,13 @@ export const en = {
524
527
  for: "for",
525
528
  since: "since",
526
529
  until: "until",
530
+ lastHeard: "last heard",
527
531
  markAbandoned: "Mark abandoned",
528
532
  waitingForDecision: "Waiting for a decision",
529
533
  answerIt: "answer it",
530
534
  reports: "Reports",
531
535
  handedIn: "Handed in",
532
- label: (agent: string, login: string) => `${agent} for ${login}`,
536
+ label: (agent: string, name: string) => `${agent} for ${name}`,
533
537
  },
534
538
 
535
539
  skills: {
@@ -621,9 +625,9 @@ export const en = {
621
625
  title: "People",
622
626
  list: "People",
623
627
  lead: "Everyone who has signed in. An administrator configures the workspace and says what each person is; a member works in the projects they are in. What a person may do, their agents may do.",
624
- roleOf: (login: string) => `Role of ${login}`,
628
+ roleOf: (name: string) => `Role of ${name}`,
625
629
  agents: "Agents",
626
- agentsOf: (login: string) => `Agents of ${login}`,
630
+ agentsOf: (name: string) => `Agents of ${name}`,
627
631
  agentsLead: (name: string) =>
628
632
  `What works here as ${name}, each with a token of its own. Disconnecting one takes its token away at once.`,
629
633
  loadingAgents: "Loading the agents",
@@ -707,7 +711,8 @@ export const en = {
707
711
  words: {
708
712
  title: "Connecting an agent",
709
713
  tellYourAgent: "Tell your agent, in its own chat:",
710
- quote: (origin: string) => `Install the console command and connect to ${origin}.`,
714
+ quote: (origin: string) =>
715
+ `Install @skillcdn/console from npm, run console login --url ${origin}, and tell me the address and the code it shows.`,
711
716
  doesTheRest:
712
717
  "An agent that can run commands does the rest: it installs the command, connects, and shows you an address with a code.",
713
718
  thenApprove:
package/src/i18n/ko.ts CHANGED
@@ -21,7 +21,7 @@ export const ko: Messages = {
21
21
  projectRole: { owner: "소유자", member: "멤버" },
22
22
  visibility: { workspace: "워크스페이스의 모두", private: "목록에 있는 사람만" },
23
23
  runStatus: {
24
- running: "작업 중",
24
+ running: "열림",
25
25
  waiting: "결정 대기 중",
26
26
  finished: "완료",
27
27
  failed: "실패",
@@ -68,6 +68,7 @@ export const ko: Messages = {
68
68
  nobody: "없음",
69
69
  nothing: "없음",
70
70
  language: "언어",
71
+ showMore: (n, of) => `${of}개 중 ${n}개 더 보기`,
71
72
  },
72
73
 
73
74
  nav: {
@@ -94,7 +95,7 @@ export const ko: Messages = {
94
95
  live: "실시간",
95
96
  reconnecting: "다시 연결하는 중",
96
97
  signOut: "로그아웃",
97
- menu: (login) => `${login}: 메뉴`,
98
+ menu: (name) => `${name}: 메뉴`,
98
99
  },
99
100
 
100
101
  signIn: {
@@ -123,7 +124,6 @@ export const ko: Messages = {
123
124
  list: "프로젝트",
124
125
  private: "비공개",
125
126
  decisionsWaiting: (n) => `결정 ${n}건 대기 중`,
126
- agentsAtWork: (n) => `에이전트 ${n}개 작업 중`,
127
127
  form: {
128
128
  aria: "새 프로젝트",
129
129
  name: "이름",
@@ -134,6 +134,11 @@ export const ko: Messages = {
134
134
  whoIsMember: "멤버 범위",
135
135
  skillsAddress: "스킬 주소",
136
136
  skillsAddressPlaceholder: "/gh/<owner>/<repo>, 비우면 조직의 스킬",
137
+ symbol: "마크",
138
+ image: "이미지",
139
+ imageRemove: "이미지 지우기",
140
+ color: "색",
141
+ colorAuto: "키에서 정해진 색",
137
142
  make: "프로젝트 만들기",
138
143
  },
139
144
  members: {
@@ -145,7 +150,7 @@ export const ko: Messages = {
145
150
  leadRoles:
146
151
  "소유자는 프로젝트를 설정하고, 멤버는 프로젝트에서 일합니다. 워크스페이스 관리자는 모든 프로젝트의 소유자입니다.",
147
152
  since: "부터",
148
- roleOf: (login) => `${login}의 역할`,
153
+ roleOf: (name) => `${name}의 역할`,
149
154
  addAria: "멤버 추가",
150
155
  person: "사람",
151
156
  chooseSomeone: "선택하세요",
@@ -179,9 +184,6 @@ export const ko: Messages = {
179
184
  moveTo: "이동",
180
185
  decisions: (n) => `결정 ${n}건`,
181
186
  decisionsTitle: "사람을 기다리는 결정",
182
- agentAtWork: "에이전트 작업 중",
183
- agentAtWorkTitle: "에이전트가 이 작업을 하고 있습니다",
184
- agentSince: (agent, since) => `${agent}, ${since}부터 작업 중`,
185
187
  filters: {
186
188
  aria: "보드 좁혀 보기",
187
189
  everyone: "모두",
@@ -219,7 +221,6 @@ export const ko: Messages = {
219
221
  raiseDecision: "결정 요청",
220
222
  nothingMore: "더 적힌 내용이 없습니다.",
221
223
  links: "링크",
222
- agentsAtWork: "작업 중인 에이전트",
223
224
  runs: "실행",
224
225
  decisions: "결정",
225
226
  history: "일어난 일",
@@ -308,8 +309,10 @@ export const ko: Messages = {
308
309
 
309
310
  calendar: {
310
311
  title: "캘린더",
311
- lead: "작업을 기한일에 놓고 봅니다. 시작일이 있는 작업은 그날부터 보이고, 날짜가 없는 작업은 아래에서 날짜를 기다립니다.",
312
+ lead: "작업을 날짜 위에 놓고 봅니다. 시작일과 기한이 있는 작업은 그 기간에 걸쳐 놓이고, 기한만 있는 작업은 그날에 놓입니다. 날짜가 없는 작업은 아래에서 날짜를 기다립니다.",
312
313
  grid: "이 달",
314
+ legend: { title: "카테고리 색" },
315
+ startsOnly: "그날 시작, 기한은 아직 없음",
313
316
  previous: "이전 달",
314
317
  next: "다음 달",
315
318
  thisMonth: "이번 달",
@@ -318,7 +321,6 @@ export const ko: Messages = {
318
321
  nothingOverdue: "기한이 지난 작업이 없습니다.",
319
322
  unscheduled: "날짜 없음",
320
323
  allScheduled: "열린 작업마다 날짜가 있습니다.",
321
- starts: "시작",
322
324
  workload: {
323
325
  title: "누가 무엇을",
324
326
  lead: "사람마다 열린 작업과, 그중 늦은 것과 일주일 안에 마감인 것.",
@@ -477,6 +479,7 @@ export const ko: Messages = {
477
479
  runReported: (task, excerpt) =>
478
480
  `${task}에 보고했습니다${excerpt === undefined ? "" : `: ${excerpt}`}`,
479
481
  runHandedIn: (label, task) => `${task}에 ${label}을(를) 제출했습니다`,
482
+ runTakenOver: (task, agent) => `${agent}에게서 ${task}을(를) 넘겨받았습니다`,
480
483
  runFinished: (task) => `${task}을(를) 끝냈습니다`,
481
484
  runFailed: (task) => `${task}에 실패했습니다`,
482
485
  runAbandoned: (task) => `${task}을(를) 포기했습니다`,
@@ -492,12 +495,13 @@ export const ko: Messages = {
492
495
  for: "대신:",
493
496
  since: "시작",
494
497
  until: "종료",
498
+ lastHeard: "마지막 소식",
495
499
  markAbandoned: "중단됨으로 표시",
496
500
  waitingForDecision: "결정을 기다리는 중",
497
501
  answerIt: "답변하기",
498
502
  reports: "보고",
499
503
  handedIn: "제출물",
500
- label: (agent, login) => `${login}을(를) 위한 ${agent}`,
504
+ label: (agent, name) => `${name}을(를) 위한 ${agent}`,
501
505
  },
502
506
 
503
507
  skills: {
@@ -589,9 +593,9 @@ export const ko: Messages = {
589
593
  title: "사람들",
590
594
  list: "사람들",
591
595
  lead: "로그인한 모든 사람. 관리자는 워크스페이스를 설정하고 각 사람의 역할을 정합니다. 멤버는 속한 프로젝트에서 일합니다. 사람이 할 수 있는 일은 그 사람의 에이전트도 할 수 있습니다.",
592
- roleOf: (login) => `${login}의 역할`,
596
+ roleOf: (name) => `${name}의 역할`,
593
597
  agents: "에이전트",
594
- agentsOf: (login) => `${login}의 에이전트`,
598
+ agentsOf: (name) => `${name}의 에이전트`,
595
599
  agentsLead: (name) =>
596
600
  `${name}(으)로 여기서 일하는 것들입니다. 각각 고유한 토큰을 가집니다. 연결을 끊으면 토큰이 즉시 사라집니다.`,
597
601
  loadingAgents: "에이전트를 불러오는 중",
@@ -674,7 +678,8 @@ export const ko: Messages = {
674
678
  words: {
675
679
  title: "에이전트 연결하기",
676
680
  tellYourAgent: "에이전트의 대화창에서 이렇게 말하세요:",
677
- quote: (origin) => `콘솔 명령을 설치하고 ${origin}에 연결해.`,
681
+ quote: (origin) =>
682
+ `npm으로 @skillcdn/console을 설치하고, console login --url ${origin} 을 실행한 뒤 나오는 주소와 코드를 알려줘.`,
678
683
  doesTheRest:
679
684
  "명령을 실행할 수 있는 에이전트는 나머지를 알아서 합니다. 명령을 설치하고, 연결하고, 코드가 있는 주소를 보여 줍니다.",
680
685
  thenApprove: