@skillcdn/console 0.1.15 → 0.1.17

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 (113) hide show
  1. package/README.md +2 -1
  2. package/dist/api.d.ts +3 -3
  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 +2 -0
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +91 -9
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +7 -2
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +23 -2
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +28 -4
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/main.js +2 -1
  18. package/dist/cli/main.js.map +1 -1
  19. package/dist/cli/terminal.d.ts +5 -0
  20. package/dist/cli/terminal.d.ts.map +1 -1
  21. package/dist/cli/terminal.js +11 -1
  22. package/dist/cli/terminal.js.map +1 -1
  23. package/dist/client.d.ts +28 -3
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +62 -2
  26. package/dist/client.js.map +1 -1
  27. package/dist/components/calendar.d.ts +35 -2
  28. package/dist/components/calendar.d.ts.map +1 -1
  29. package/dist/components/calendar.js +58 -19
  30. package/dist/components/calendar.js.map +1 -1
  31. package/dist/components/comments.d.ts +59 -0
  32. package/dist/components/comments.d.ts.map +1 -0
  33. package/dist/components/comments.js +138 -0
  34. package/dist/components/comments.js.map +1 -0
  35. package/dist/components/event-feed.d.ts.map +1 -1
  36. package/dist/components/event-feed.js +2 -0
  37. package/dist/components/event-feed.js.map +1 -1
  38. package/dist/components/markdown.d.ts +6 -0
  39. package/dist/components/markdown.d.ts.map +1 -1
  40. package/dist/components/markdown.js +2 -1
  41. package/dist/components/markdown.js.map +1 -1
  42. package/dist/components/projects.d.ts +4 -1
  43. package/dist/components/projects.d.ts.map +1 -1
  44. package/dist/components/projects.js +10 -2
  45. package/dist/components/projects.js.map +1 -1
  46. package/dist/components/task-form.d.ts.map +1 -1
  47. package/dist/components/task-form.js +1 -1
  48. package/dist/components/task-form.js.map +1 -1
  49. package/dist/components/task-view.d.ts +8 -3
  50. package/dist/components/task-view.d.ts.map +1 -1
  51. package/dist/components/task-view.js +6 -4
  52. package/dist/components/task-view.js.map +1 -1
  53. package/dist/console.css +180 -32
  54. package/dist/console.d.ts.map +1 -1
  55. package/dist/console.js +23 -5
  56. package/dist/console.js.map +1 -1
  57. package/dist/data.d.ts +5 -1
  58. package/dist/data.d.ts.map +1 -1
  59. package/dist/data.js +2 -0
  60. package/dist/data.js.map +1 -1
  61. package/dist/i18n/en.d.ts +15 -2
  62. package/dist/i18n/en.d.ts.map +1 -1
  63. package/dist/i18n/en.js +17 -4
  64. package/dist/i18n/en.js.map +1 -1
  65. package/dist/i18n/ko.d.ts.map +1 -1
  66. package/dist/i18n/ko.js +17 -4
  67. package/dist/i18n/ko.js.map +1 -1
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +1 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/limits.d.ts +3 -0
  73. package/dist/limits.d.ts.map +1 -1
  74. package/dist/limits.js +3 -0
  75. package/dist/limits.js.map +1 -1
  76. package/dist/rules.js +2 -2
  77. package/dist/rules.js.map +1 -1
  78. package/dist/schemas.d.ts +105 -0
  79. package/dist/schemas.d.ts.map +1 -1
  80. package/dist/schemas.js +32 -1
  81. package/dist/schemas.js.map +1 -1
  82. package/dist/vocabulary.d.ts +1 -1
  83. package/dist/vocabulary.d.ts.map +1 -1
  84. package/dist/vocabulary.js +1 -0
  85. package/dist/vocabulary.js.map +1 -1
  86. package/docs/specs/cli.md +8 -4
  87. package/docs/specs/rest.md +9 -2
  88. package/package.json +1 -1
  89. package/skills/working-the-board/SKILL.md +6 -4
  90. package/src/api.ts +12 -0
  91. package/src/cli/cli.ts +106 -9
  92. package/src/cli/format.ts +28 -1
  93. package/src/cli/help.ts +32 -3
  94. package/src/cli/main.ts +2 -1
  95. package/src/cli/terminal.ts +12 -1
  96. package/src/client.ts +112 -3
  97. package/src/components/calendar.tsx +216 -28
  98. package/src/components/comments.tsx +342 -0
  99. package/src/components/event-feed.tsx +2 -0
  100. package/src/components/markdown.tsx +9 -1
  101. package/src/components/projects.tsx +19 -6
  102. package/src/components/task-form.tsx +1 -0
  103. package/src/components/task-view.tsx +32 -33
  104. package/src/console.tsx +27 -3
  105. package/src/data.ts +8 -0
  106. package/src/i18n/en.ts +19 -4
  107. package/src/i18n/ko.ts +19 -4
  108. package/src/index.ts +12 -0
  109. package/src/limits.ts +3 -0
  110. package/src/rules.ts +2 -2
  111. package/src/schemas.ts +40 -1
  112. package/src/styles/console.css +180 -32
  113. package/src/vocabulary.ts +1 -0
@@ -76,6 +76,8 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
76
76
  );
77
77
  case "task.updated":
78
78
  return feed.event.taskUpdated(fields, task);
79
+ case "task.commented":
80
+ return feed.event.taskCommented(task, data.excerpt);
79
81
  case "decision.raised":
80
82
  return feed.event.decisionRaised(question);
81
83
  case "decision.answered":
@@ -14,6 +14,12 @@ export interface MarkdownProps {
14
14
  readonly source: string;
15
15
  /** Where a document of the project is read, by its path; left out, such links stay text. */
16
16
  readonly docHref?: ((path: string) => string) | undefined;
17
+ /**
18
+ * Where a picture named in the text is read, when the text comes with files: given the
19
+ * source of `![words](source)`, the address of the file it names, or nothing. A picture that
20
+ * is neither one of those nor over https stays text.
21
+ */
22
+ readonly fileSrc?: ((source: string) => string | undefined) | undefined;
17
23
  }
18
24
 
19
25
  export function Markdown(props: MarkdownProps) {
@@ -48,7 +54,9 @@ export function Markdown(props: MarkdownProps) {
48
54
  return <span>{children}</span>;
49
55
  },
50
56
  img({ src, alt, title }) {
51
- const url = typeof src === "string" && SAFE_IMAGE.test(src) ? src : undefined;
57
+ const named = typeof src === "string" ? props.fileSrc?.(src) : undefined;
58
+ const url =
59
+ named ?? (typeof src === "string" && SAFE_IMAGE.test(src) ? src : undefined);
52
60
  if (url === undefined) {
53
61
  return <span className="sc-prose-image-text">{alt ?? ""}</span>;
54
62
  }
@@ -25,7 +25,10 @@ import {
25
25
  import { useWords } from "../i18n/index.js";
26
26
  import { Badge, Button, cx, PersonChip, personName, Time } from "./ui.js";
27
27
 
28
- /** A project's mark: its symbol on its colour, so that projects tell apart at a glance. */
28
+ /**
29
+ * A project's mark: its symbol on its colour, so that projects tell apart at a glance; its own
30
+ * image over the symbol where it has one, so that the mark is never empty while the image loads.
31
+ */
29
32
  export function ProjectMark(props: {
30
33
  readonly project: {
31
34
  readonly key: string;
@@ -43,20 +46,30 @@ export function ProjectMark(props: {
43
46
  const icon = props.project.icon ?? null;
44
47
  const src =
45
48
  props.iconSrc ?? (icon === null ? undefined : projectIconPath(props.project.key, icon));
49
+ // The symbol stands in until the image is there, and comes back should the image fail; once
50
+ // the image is shown, the symbol goes, since an image with clear parts would show it through.
51
+ const [shown, setShown] = useState<string | undefined>(undefined);
52
+ const [failed, setFailed] = useState<string | undefined>(undefined);
53
+ const pictured = src !== undefined && failed !== src;
46
54
  return (
47
55
  <span
48
56
  className={cx(
49
57
  "sc-mark",
50
58
  `sc-mark-${size}`,
51
59
  `sc-category-${projectColorOf(props.project)}`,
52
- src !== undefined && "sc-mark-pictured",
60
+ pictured && "sc-mark-pictured",
53
61
  )}
54
62
  aria-hidden="true"
55
63
  >
56
- {src === undefined ? (
57
- projectSymbolOf(props.project)
58
- ) : (
59
- <img className="sc-mark-image" src={src} alt="" />
64
+ {(!pictured || shown !== src) && projectSymbolOf(props.project)}
65
+ {pictured && (
66
+ <img
67
+ className="sc-mark-image"
68
+ src={src}
69
+ alt=""
70
+ onLoad={() => setShown(src)}
71
+ onError={() => setFailed(src)}
72
+ />
60
73
  )}
61
74
  </span>
62
75
  );
@@ -96,6 +96,7 @@ export function TaskForm(props: TaskFormProps) {
96
96
  <input
97
97
  className="sc-input"
98
98
  value={title}
99
+ placeholder={form.titlePlaceholder}
99
100
  maxLength={MAX_TITLE_LENGTH}
100
101
  required
101
102
  onChange={(event) => setTitle(event.target.value)}
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  RestAnswerInput,
3
- RestArtifact,
4
3
  RestCategory,
4
+ RestComment,
5
5
  RestDecision,
6
6
  RestDecisionInput,
7
7
  RestDecisionPatch,
@@ -17,19 +17,20 @@ import { formatDay } from "../dates.js";
17
17
  import { useLanguage, useWords } from "../i18n/index.js";
18
18
  import type { TaskFormKind } from "../router.js";
19
19
  import { TASK_STATES } from "../vocabulary.js";
20
+ import { type CommentDraft, TaskThread } from "./comments.js";
20
21
  import { DecisionForm } from "./decision-form.js";
21
- import { DecisionCard } from "./decision-list.js";
22
22
  import { EventFeed } from "./event-feed.js";
23
23
  import { Markdown } from "./markdown.js";
24
- import { RunList } from "./runs.js";
25
24
  import { TaskForm } from "./task-form.js";
26
25
  import { CategoryBadge, DueBadge, PersonChip, PriorityBadge, StateBadge, Time } from "./ui.js";
27
26
 
28
- // One task: what it is, who it is on, what it links to, the agents at work on it, the decisions
29
- // about it, the tasks that are part of it, everything that happened to it, and the ways to
30
- // change it. Takes its data as props and nothing from the network; which of its forms is open
31
- // is told by the page, since each form has an address of its own (ADR-0010). The facts beside
32
- // the text say only what is set: a plan, a category or a goal the task has none of is not a row.
27
+ // One task: what it is, who it is on, what it links to, the discussion on it, which is what was
28
+ // said about it with the agents at work on it and the decisions about it in the order they
29
+ // came (ADR-0024), the tasks that are part of it, everything that happened to it, and the ways
30
+ // to change it. Takes its data as props and nothing from the network; which of its forms is
31
+ // open is told by the page, since each form has an address of its own (ADR-0010). The facts
32
+ // beside the text say only what is set: a plan, a category or a goal the task has none of is
33
+ // not a row.
33
34
 
34
35
  export interface TaskViewProps {
35
36
  readonly task: RestTask;
@@ -39,6 +40,8 @@ export interface TaskViewProps {
39
40
  readonly decisions: readonly RestDecision[];
40
41
  /** The runs of the board, or of this task; the ones on this task are shown. */
41
42
  readonly runs?: readonly RestRun[] | undefined;
43
+ /** The comments on the task, oldest first, when the page has read them. */
44
+ readonly comments?: readonly RestComment[] | undefined;
42
45
  /** Everything that happened to the task, oldest first, when the page has read it. */
43
46
  readonly history?: readonly RestEvent[] | undefined;
44
47
  /** The categories of the workspace, for the form. */
@@ -50,8 +53,8 @@ export interface TaskViewProps {
50
53
  readonly decisionHref?: ((decisionId: string) => string) | undefined;
51
54
  /** Where a goal of the roadmap is read. */
52
55
  readonly goalHref?: ((goalId: string) => string) | undefined;
53
- /** Where a file a run handed in is read. */
54
- readonly fileHref?: ((artifact: RestArtifact) => string) | undefined;
56
+ /** Where a file of the project is read, by its id: one a run handed in, or one put in a comment. */
57
+ readonly fileHref?: ((fileId: string) => string) | undefined;
55
58
  /** Where a document of the project is read, by its path, for the links in what was written. */
56
59
  readonly docHref?: ((path: string) => string) | undefined;
57
60
  /** The day to count due days from; the browser's today when left out. */
@@ -73,6 +76,8 @@ export interface TaskViewProps {
73
76
  | ((decision: RestDecision, patch: RestDecisionPatch) => void)
74
77
  | undefined;
75
78
  readonly onAbandonRun?: ((run: RestRun) => void) | undefined;
79
+ /** Called with a word for the discussion, and the files put in it. Left out, nothing can be said here. */
80
+ readonly onComment?: ((draft: CommentDraft) => void) | undefined;
76
81
  }
77
82
 
78
83
  export function TaskView(props: TaskViewProps) {
@@ -86,7 +91,7 @@ export function TaskView(props: TaskViewProps) {
86
91
  const subtasks = props.tasks.filter((candidate) => candidate.parentId === task.id);
87
92
  const about = props.decisions.filter((decision) => decision.taskId === task.id);
88
93
  const runs = (props.runs ?? []).filter((run) => run.taskId === task.id);
89
- const tasksById = new Map(props.tasks.map((candidate) => [candidate.id, candidate]));
94
+ const comments = (props.comments ?? []).filter((comment) => comment.taskId === task.id);
90
95
  const goal =
91
96
  task.goalId === null
92
97
  ? undefined
@@ -175,35 +180,29 @@ export function TaskView(props: TaskViewProps) {
175
180
  />
176
181
  </section>
177
182
  )}
178
- {runs.length > 0 && (
179
- <section className="sc-task-section" aria-label={words.task.runs}>
180
- <h2 className="sc-section-title">{words.task.runs}</h2>
181
- <RunList
183
+ {(runs.length > 0 ||
184
+ about.length > 0 ||
185
+ comments.length > 0 ||
186
+ props.onComment !== undefined) && (
187
+ <section className="sc-task-section" aria-label={words.comments.title}>
188
+ <h2 className="sc-section-title">{words.comments.title}</h2>
189
+ <TaskThread
190
+ task={task}
191
+ comments={comments}
182
192
  runs={runs}
183
- decisionHref={props.decisionHref}
193
+ decisions={about}
184
194
  fileHref={props.fileHref}
195
+ decisionHref={props.decisionHref}
185
196
  docHref={props.docHref}
186
- onAbandon={props.onAbandonRun}
197
+ onAnswer={props.onAnswer}
198
+ onUpdateDecision={props.onUpdateDecision}
199
+ onAbandonRun={props.onAbandonRun}
200
+ onComment={props.onComment}
187
201
  busy={props.busy}
202
+ empty={<p className="sc-muted">{words.comments.empty}</p>}
188
203
  />
189
204
  </section>
190
205
  )}
191
- {about.length > 0 && (
192
- <section className="sc-task-section" aria-label={words.task.decisions}>
193
- <h2 className="sc-section-title">{words.task.decisions}</h2>
194
- {about.map((decision) => (
195
- <DecisionCard
196
- key={decision.id}
197
- decision={decision}
198
- task={decision.taskId === null ? undefined : tasksById.get(decision.taskId)}
199
- docHref={props.docHref}
200
- onAnswer={props.onAnswer}
201
- onUpdate={props.onUpdateDecision}
202
- busy={props.busy}
203
- />
204
- ))}
205
- </section>
206
- )}
207
206
  {props.history !== undefined && props.history.length > 0 && (
208
207
  <section className="sc-task-section" aria-label={words.task.history}>
209
208
  <h2 className="sc-section-title">{words.task.history}</h2>
package/src/console.tsx CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  PROJECT_RULES_TEMPLATE,
18
18
  REFERENCE_RULES,
19
19
  type RestAnswerInput,
20
+ type RestComment,
20
21
  type RestDecision,
21
22
  type RestDecisionInput,
22
23
  type RestDecisionPatch,
@@ -457,7 +458,8 @@ function TaskPage(props: {
457
458
  ? data.tasks.find((candidate) => candidate.id === route.id)
458
459
  : data.tasks.find((candidate) => candidate.number === route.number);
459
460
  const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
460
- const { taskHistory } = data.actions;
461
+ const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
462
+ const { taskHistory, taskComments } = data.actions;
461
463
  const taskId = task?.id;
462
464
  const number = task?.number;
463
465
  // A task named by its id is shown at its number's address.
@@ -481,8 +483,15 @@ function TaskPage(props: {
481
483
  }
482
484
  })
483
485
  .catch(() => undefined);
486
+ taskComments(taskId, controller.signal)
487
+ .then((items) => {
488
+ if (!controller.signal.aborted) {
489
+ setComments(items);
490
+ }
491
+ })
492
+ .catch(() => undefined);
484
493
  return () => controller.abort();
485
- }, [taskId, taskHistory, feedLength]);
494
+ }, [taskId, taskHistory, taskComments, feedLength]);
486
495
  if (task === undefined) {
487
496
  return (
488
497
  <EmptyState
@@ -500,13 +509,14 @@ function TaskPage(props: {
500
509
  tasks={data.tasks}
501
510
  decisions={data.decisions}
502
511
  runs={data.runs}
512
+ comments={comments}
503
513
  history={history}
504
514
  categories={data.categories}
505
515
  goals={data.goals}
506
516
  taskHref={(candidate) => taskHref(key, candidate.number)}
507
517
  decisionHref={(decisionId) => decisionHref(key, decisionId)}
508
518
  goalHref={(goalId) => goalHref(key, goalId)}
509
- fileHref={(artifact) => projectPath(key, "files", artifact.id)}
519
+ fileHref={(fileId) => projectPath(key, "files", fileId)}
510
520
  docHref={(path) => docHref(key, path)}
511
521
  form={route.form}
512
522
  formHref={(form) => taskHref(key, task.number, form)}
@@ -533,6 +543,19 @@ function TaskPage(props: {
533
543
  void act(() => data.actions.updateDecision(decision.id, patch))
534
544
  }
535
545
  onAbandonRun={(run) => void act(() => data.actions.abandonRun(run.id))}
546
+ onComment={(draft) =>
547
+ void act(async () => {
548
+ await data.actions.comment(task.id, {
549
+ body: draft.body,
550
+ files: draft.files.map((file) => ({
551
+ name: file.name,
552
+ bytes: file,
553
+ ...(file.type === "" ? {} : { contentType: file.type }),
554
+ })),
555
+ });
556
+ setComments(await taskComments(task.id));
557
+ })
558
+ }
536
559
  />
537
560
  );
538
561
  }
@@ -677,6 +700,7 @@ function ProjectPage(props: {
677
700
  month={month}
678
701
  today={now}
679
702
  categories={data.categories}
703
+ goals={data.goals}
680
704
  taskHref={href}
681
705
  monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
682
706
  />
package/src/data.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
3
  ApiError,
4
+ type CommentInput,
4
5
  type ConsoleClient,
5
6
  type DocumentFilter,
6
7
  EVENT_KINDS,
@@ -9,6 +10,7 @@ import {
9
10
  type RestCategory,
10
11
  type RestCategoryInput,
11
12
  type RestCategoryPatch,
13
+ type RestComment,
12
14
  type RestConnectRequest,
13
15
  type RestDecision,
14
16
  type RestDecisionInput,
@@ -117,6 +119,10 @@ export interface ConsoleActions {
117
119
  abandonRun(id: string): Promise<RestRun>;
118
120
  /** Everything that happened to one task of the project, oldest first. */
119
121
  taskHistory(taskId: string, signal?: AbortSignal): Promise<RestEvent[]>;
122
+ /** The discussion on one task of the project, oldest first (ADR-0024). */
123
+ taskComments(taskId: string, signal?: AbortSignal): Promise<RestComment[]>;
124
+ /** A word in the discussion on a task, with the files put in it. */
125
+ comment(taskId: string, input: CommentInput): Promise<RestComment>;
120
126
  /** Makes a token for whoever is signed in; the answer carries the secret, this once. */
121
127
  createToken(input: RestTokenInput): Promise<RestTokenCreated>;
122
128
  revokeToken(id: string): Promise<void>;
@@ -560,6 +566,8 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
560
566
  }
561
567
  return history;
562
568
  },
569
+ taskComments: async (taskId, signal) => (await scope().comments(taskId, signal)).items,
570
+ comment: (taskId, input) => scope().comment(taskId, input),
563
571
  async createToken(input) {
564
572
  const made = await client.createToken(input);
565
573
  setTokens((await client.tokens()).items);
package/src/i18n/en.ts CHANGED
@@ -131,7 +131,7 @@ export const en = {
131
131
  },
132
132
 
133
133
  signIn: {
134
- lead: "The board of the work, the agents at it, what they did, and the decisions that wait for a person.",
134
+ lead: "Join an organization made of AI agents.",
135
135
  continueWith: (provider: string) => `Continue with ${provider}`,
136
136
  nobody: {
137
137
  title: "Nobody can sign in here yet",
@@ -249,8 +249,6 @@ export const en = {
249
249
  raiseDecision: "Raise a decision",
250
250
  nothingMore: "Nothing more was written about it.",
251
251
  links: "Links",
252
- runs: "Runs",
253
- decisions: "Decisions",
254
252
  history: "What happened",
255
253
  subtasks: "Subtasks",
256
254
  partOfIt: "Part of it",
@@ -269,6 +267,7 @@ export const en = {
269
267
  done: "Done",
270
268
  form: {
271
269
  title: "Title",
270
+ titlePlaceholder: "What is to be done, in plain words that stand on their own.",
272
271
  body: "Body",
273
272
  bodyPlaceholder: "What is to be done, in Markdown.",
274
273
  state: "State",
@@ -299,7 +298,7 @@ export const en = {
299
298
  body: "It may belong to another project, or the link is wrong.",
300
299
  },
301
300
  empty: {
302
- title: "Nothing waits for a person",
301
+ title: "No decision waits for a person",
303
302
  body: "A decision raised from a task, or from here, shows up on this page.",
304
303
  },
305
304
  waiting: "Waiting",
@@ -537,6 +536,8 @@ export const en = {
537
536
  taskCreated: (task: string) => `wrote ${task}`,
538
537
  taskMoved: (task: string, from: string, to: string) => `moved ${task} from ${from} to ${to}`,
539
538
  taskUpdated: (fields: string, task: string) => `changed ${fields} of ${task}`,
539
+ taskCommented: (task: string, excerpt: string | undefined) =>
540
+ `commented on ${task}${excerpt === undefined || excerpt === "" ? "" : `: ${excerpt}`}`,
540
541
  decisionRaised: (question: string) => `asked: ${question}`,
541
542
  decisionAnswered: (question: string, option: string) => `answered "${question}": ${option}`,
542
543
  decisionUpdated: (fields: string, question: string) =>
@@ -570,6 +571,20 @@ export const en = {
570
571
  label: (agent: string, name: string) => `${agent} for ${name}`,
571
572
  },
572
573
 
574
+ comments: {
575
+ title: "Discussion",
576
+ empty: "Nothing was said yet.",
577
+ one: "Comment",
578
+ write: "Write a comment",
579
+ placeholder: "A word in the discussion, in Markdown.",
580
+ files: "Files",
581
+ chooseFiles: "Add files",
582
+ hint: "A picture pasted or chosen here goes with the comment; ![words](its name) places it in the text.",
583
+ removeFile: (name: string) => `Take ${name} out`,
584
+ picture: (name: string) => `The picture ${name}`,
585
+ label: (name: string) => `A comment by ${name}`,
586
+ },
587
+
573
588
  skills: {
574
589
  loading: "Loading the skills",
575
590
  list: "Skills",
package/src/i18n/ko.ts CHANGED
@@ -101,7 +101,7 @@ export const ko: Messages = {
101
101
  },
102
102
 
103
103
  signIn: {
104
- lead: "할 일의 보드, 일하는 에이전트, 그들이 한 일, 그리고 사람을 기다리는 결정.",
104
+ lead: "AI 에이전트로 이뤄진 조직에 합류하세요.",
105
105
  continueWith: (provider) => `${provider}로 계속`,
106
106
  nobody: {
107
107
  title: "아직 아무도 로그인할 수 없습니다",
@@ -218,8 +218,6 @@ export const ko: Messages = {
218
218
  raiseDecision: "결정 요청",
219
219
  nothingMore: "더 적힌 내용이 없습니다.",
220
220
  links: "링크",
221
- runs: "실행",
222
- decisions: "결정",
223
221
  history: "일어난 일",
224
222
  subtasks: "하위 작업",
225
223
  partOfIt: "하위 작업",
@@ -238,6 +236,7 @@ export const ko: Messages = {
238
236
  done: "완료",
239
237
  form: {
240
238
  title: "제목",
239
+ titlePlaceholder: "무엇을 할지, 제목만 보고도 알 수 있게.",
241
240
  body: "본문",
242
241
  bodyPlaceholder: "무엇을 할지, Markdown으로.",
243
242
  state: "상태",
@@ -268,7 +267,7 @@ export const ko: Messages = {
268
267
  body: "다른 프로젝트의 것이거나, 링크가 잘못됐습니다.",
269
268
  },
270
269
  empty: {
271
- title: "사람을 기다리는 것이 없습니다",
270
+ title: "사람을 기다리는 결정이 없습니다",
272
271
  body: "작업에서, 또는 여기서 요청한 결정이 이 페이지에 나타납니다.",
273
272
  },
274
273
  waiting: "대기 중",
@@ -503,6 +502,8 @@ export const ko: Messages = {
503
502
  taskCreated: (task) => `${task}을(를) 썼습니다`,
504
503
  taskMoved: (task, from, to) => `${task}을(를) ${from}에서 ${to}(으)로 옮겼습니다`,
505
504
  taskUpdated: (fields, task) => `${task}의 ${fields}을(를) 바꿨습니다`,
505
+ taskCommented: (task, excerpt) =>
506
+ `${task}에 댓글을 남겼습니다${excerpt === undefined || excerpt === "" ? "" : `: ${excerpt}`}`,
506
507
  decisionRaised: (question) => `질문했습니다: ${question}`,
507
508
  decisionAnswered: (question, option) => `"${question}"에 답했습니다: ${option}`,
508
509
  decisionUpdated: (fields, question) => `결정 "${question}"의 ${fields}을(를) 썼습니다`,
@@ -535,6 +536,20 @@ export const ko: Messages = {
535
536
  label: (agent, name) => `${name}을(를) 위한 ${agent}`,
536
537
  },
537
538
 
539
+ comments: {
540
+ title: "논의",
541
+ empty: "아직 아무 말도 없습니다.",
542
+ one: "댓글 남기기",
543
+ write: "댓글 쓰기",
544
+ placeholder: "논의에 보탤 말을 마크다운으로.",
545
+ files: "파일",
546
+ chooseFiles: "파일 추가",
547
+ hint: "여기에 붙여 넣거나 고른 그림은 댓글과 함께 올라갑니다. ![설명](파일 이름)으로 본문에 넣을 수 있습니다.",
548
+ removeFile: (name) => `${name} 빼기`,
549
+ picture: (name) => `그림 ${name}`,
550
+ label: (name) => `${name}의 댓글`,
551
+ },
552
+
538
553
  skills: {
539
554
  loading: "스킬을 불러오는 중",
540
555
  list: "스킬",
package/src/index.ts CHANGED
@@ -26,6 +26,18 @@ export {
26
26
  workloadOf,
27
27
  } from "./components/calendar.js";
28
28
  export { CategoryList, type CategoryListProps } from "./components/categories.js";
29
+ export {
30
+ CommentCard,
31
+ type CommentCardProps,
32
+ type CommentDraft,
33
+ CommentForm,
34
+ type CommentFormProps,
35
+ isPicture,
36
+ TaskThread,
37
+ type TaskThreadProps,
38
+ type ThreadItem,
39
+ threadOf,
40
+ } from "./components/comments.js";
29
41
  export {
30
42
  ConnectApproval,
31
43
  type ConnectApprovalProps,
package/src/limits.ts CHANGED
@@ -28,6 +28,9 @@ export const MAX_SUMMARY_LENGTH = 2000;
28
28
  /** How many reports, and how many artifacts, one run may carry. */
29
29
  export const MAX_REPORTS_PER_RUN = 500;
30
30
  export const MAX_ARTIFACTS_PER_RUN = 50;
31
+ /** How many comments one task carries, and how many files one comment carries, together within `MAX_FILE_BYTES`. */
32
+ export const MAX_COMMENTS_PER_TASK = 500;
33
+ export const MAX_FILES_PER_COMMENT = 10;
31
34
  /** A file a run hands in: how many bytes, and its name, one line without a path. */
32
35
  export const MAX_FILE_BYTES = 10 * 1024 * 1024;
33
36
  export const MAX_FILE_NAME_LENGTH = 200;
package/src/rules.ts CHANGED
@@ -23,7 +23,7 @@ export const REFERENCE_RULES = [
23
23
  "",
24
24
  "## Start here, every time",
25
25
  "",
26
- "1. These rules come with the task: `console take` prints them under it, and `console rules` reads them any time. Read them before anything is made. The task says where the work is and what it links to; `console task <number>` shows a task in full, with its plan, its decisions and the runs on it, without taking it. How the board is worked is the skill working-the-board, shipped with the command (`console skills` says where it is); these rules say what this organization asks, not how the command works.",
26
+ "1. These rules come with the task: `console take` prints them under it, and `console rules` reads them any time. Read them before anything is made. The task says where the work is and what it links to; `console task <number>` shows a task in full, with its plan, its decisions, the runs on it and the discussion on it, without taking it. How the board is worked is the skill working-the-board, shipped with the command (`console skills` says where it is); these rules say what this organization asks, not how the command works.",
27
27
  "2. Act as the person whose token the command holds, and no further. What they may not do, the console refuses; that step goes back to them in a report, never around them.",
28
28
  "",
29
29
  "## The organization's skills",
@@ -44,7 +44,7 @@ export const REFERENCE_RULES = [
44
44
  "",
45
45
  "## Reports",
46
46
  "",
47
- "Report at the milestones, not every step: what was found, what was done, what is next, in words for people; a problem as a problem, with what it blocks. What people read on the board, the reports, the questions and their options, the tasks and the pages, is written in {language}. The result goes in a last report or is handed in as a link or a file, with the check that ran and its result; a check that did not run is said to be not run.",
47
+ "Report at the milestones, not every step: what was found, what was done, what is next, in words for people; a problem as a problem, with what it blocks. What people read on the board, the reports, the questions and their options, the tasks and the pages, is written in {language}. A task's title says what is to be done, in plain words that stand on their own; what it refers to, a milestone, a commit, a page, goes in the body, linked where it can be. The result goes in a last report or is handed in as a link or a file, with the check that ran and its result; a check that did not run is said to be not run. A report is the run's record; a word to the task's people, an answer to what they asked in its comments, a question that is not a decision, a remark, is a comment on the task (`console comment`), and what they put in a comment is read with `console file`.",
48
48
  "",
49
49
  "## What never happens",
50
50
  "",
package/src/schemas.ts CHANGED
@@ -647,8 +647,10 @@ export const restEventDataSchema = z.object({
647
647
  status: z.optional(z.enum(RUN_STATUSES)),
648
648
  /** For `run.handed_in`: what the artifact is called. */
649
649
  label: z.optional(z.string()),
650
- /** For `run.reported`: the first words of the report. */
650
+ /** For `run.reported` and `task.commented`: the first words of what was written. */
651
651
  excerpt: z.optional(z.string()),
652
+ /** For `task.commented`: how many files came with it. */
653
+ files: z.optional(z.int()),
652
654
  });
653
655
  export type RestEventData = z.infer<typeof restEventDataSchema>;
654
656
 
@@ -740,6 +742,34 @@ export type RestRun = z.infer<typeof restRunSchema>;
740
742
  export const restRunsSchema = z.object({ items: z.array(restRunSchema), more: z.boolean() });
741
743
  export type RestRuns = z.infer<typeof restRunsSchema>;
742
744
 
745
+ /** A file put in a comment, kept as a run's file is; its bytes are at `projectPath(key, "files", <id>)`. */
746
+ export const restCommentFileSchema = z.object({
747
+ id: uuid,
748
+ file: restFileSchema,
749
+ });
750
+ export type RestCommentFile = z.infer<typeof restCommentFileSchema>;
751
+
752
+ /**
753
+ * A word in the discussion on a task (ADR-0024): by a person, or by their agent, with the
754
+ * files put in it. Never rewritten: a mistake is corrected in the next one.
755
+ */
756
+ export const restCommentSchema = z.object({
757
+ id: uuid,
758
+ taskId: uuid,
759
+ author: restPersonSchema,
760
+ /** The agent the author wrote through, or `null` when a person wrote it themselves. */
761
+ agent: z.nullable(z.string()),
762
+ /** Markdown. A picture among the files is placed in it as `![words](its name)`. */
763
+ body: z.string(),
764
+ files: z.array(restCommentFileSchema),
765
+ createdAt: instant,
766
+ });
767
+ export type RestComment = z.infer<typeof restCommentSchema>;
768
+
769
+ /** `GET .../tasks/<id>/comments`: the discussion on a task, oldest first, whole. */
770
+ export const restCommentsSchema = z.object({ items: z.array(restCommentSchema) });
771
+ export type RestComments = z.infer<typeof restCommentsSchema>;
772
+
743
773
  /** `GET .../events?after=`: what happened after that number, oldest first. */
744
774
  export const restEventsSchema = z.object({
745
775
  items: z.array(restEventSchema),
@@ -1032,6 +1062,15 @@ export const restReportInputSchema = z.object({
1032
1062
  });
1033
1063
  export type RestReportInput = z.infer<typeof restReportInputSchema>;
1034
1064
 
1065
+ /**
1066
+ * What `POST .../tasks/<id>/comments` is sent as JSON: the word, in Markdown. As a form, the
1067
+ * part `body` says the same and the parts `file` carry the files; then the body may be blank.
1068
+ */
1069
+ export const restCommentInputSchema = z.object({
1070
+ body,
1071
+ });
1072
+ export type RestCommentInput = z.infer<typeof restCommentInputSchema>;
1073
+
1035
1074
  /** What `POST .../runs/<id>/artifacts` is sent: a link to what was made. */
1036
1075
  export const restArtifactInputSchema = z.object({
1037
1076
  url,