@skillcdn/console 0.1.16 → 0.1.18
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.
- package/README.md +4 -3
- package/dist/api.d.ts +4 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -2
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +12 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +226 -21
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +8 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +31 -5
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +34 -8
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/install.d.ts +21 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/install.js +87 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/main.js +24 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/notices.d.ts +13 -0
- package/dist/cli/notices.d.ts.map +1 -0
- package/dist/cli/notices.js +31 -0
- package/dist/cli/notices.js.map +1 -0
- package/dist/cli/terminal.d.ts +5 -0
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +11 -1
- package/dist/cli/terminal.js.map +1 -1
- package/dist/client.d.ts +43 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +69 -4
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +66 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +115 -0
- package/dist/compat.js.map +1 -0
- package/dist/components/calendar.d.ts +3 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +21 -2
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/comments.d.ts +59 -0
- package/dist/components/comments.d.ts.map +1 -0
- package/dist/components/comments.js +138 -0
- package/dist/components/comments.js.map +1 -0
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +2 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +3 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +23 -11
- package/dist/components/goals.js.map +1 -1
- package/dist/components/markdown.d.ts +6 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +2 -1
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +6 -1
- package/dist/components/projects.js.map +1 -1
- package/dist/components/roadmap.d.ts +37 -0
- package/dist/components/roadmap.d.ts.map +1 -0
- package/dist/components/roadmap.js +76 -0
- package/dist/components/roadmap.js.map +1 -0
- package/dist/components/task-view.d.ts +8 -3
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/tokens.d.ts +2 -0
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +13 -3
- package/dist/components/tokens.js.map +1 -1
- package/dist/console.css +314 -2
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +30 -11
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +5 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +3 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +38 -2
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +38 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +38 -2
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +3 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +3 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +6 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +10 -1
- package/dist/router.js.map +1 -1
- package/dist/rules.js +2 -2
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +147 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +50 -1
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +1 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +17 -10
- package/docs/specs/rest.md +17 -6
- package/package.json +3 -2
- package/skills/working-the-board/SKILL.md +8 -5
- package/src/api.ts +35 -0
- package/src/cli/cli.ts +273 -22
- package/src/cli/format.ts +36 -4
- package/src/cli/help.ts +38 -7
- package/src/cli/install.ts +111 -0
- package/src/cli/main.ts +25 -4
- package/src/cli/notices.ts +48 -0
- package/src/cli/terminal.ts +12 -1
- package/src/client.ts +142 -8
- package/src/compat.ts +173 -0
- package/src/components/calendar.tsx +41 -1
- package/src/components/comments.tsx +342 -0
- package/src/components/event-feed.tsx +2 -0
- package/src/components/goals.tsx +86 -7
- package/src/components/markdown.tsx +9 -1
- package/src/components/projects.tsx +16 -3
- package/src/components/roadmap.tsx +242 -0
- package/src/components/task-view.tsx +32 -33
- package/src/components/tokens.tsx +38 -1
- package/src/console.tsx +45 -3
- package/src/data.ts +9 -1
- package/src/i18n/en.ts +40 -2
- package/src/i18n/ko.ts +40 -2
- package/src/index.ts +20 -0
- package/src/limits.ts +3 -0
- package/src/router.ts +13 -2
- package/src/rules.ts +2 -2
- package/src/schemas.ts +60 -1
- package/src/styles/console.css +314 -2
- package/src/vocabulary.ts +1 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { RestCategory, RestGoal } from "../api.js";
|
|
2
|
+
import { addMonths, daysBetween, formatMonth, monthOf, today as todayOf } from "../dates.js";
|
|
3
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
4
|
+
import { cx, GoalStateBadge } from "./ui.js";
|
|
5
|
+
|
|
6
|
+
// The roadmap as months (ADR-0026): the months as columns, and each goal planned in days a bar
|
|
7
|
+
// from its start to its target in the colour of its category, filled as far as its tasks are
|
|
8
|
+
// done, with today a line down the months. A goal with one day alone sits on it as a point; one
|
|
9
|
+
// with no day is named below the months, since a plan without days is on none of them. Takes
|
|
10
|
+
// its data as props and nothing from the network; the months shown are the address's (ADR-0010).
|
|
11
|
+
|
|
12
|
+
/** How many months the roadmap shows at once when the address names no other span. */
|
|
13
|
+
export const DEFAULT_TIMELINE_MONTHS = 6;
|
|
14
|
+
|
|
15
|
+
export interface TimelineBar {
|
|
16
|
+
readonly goal: RestGoal;
|
|
17
|
+
/** Where the bar begins and ends, as shares of the whole span, 0 to 1. */
|
|
18
|
+
readonly from: number;
|
|
19
|
+
readonly to: number;
|
|
20
|
+
/** Whether the goal begins within the span, and whether it ends within it. */
|
|
21
|
+
readonly begins: boolean;
|
|
22
|
+
readonly ends: boolean;
|
|
23
|
+
/** Whether the goal has one day only: a point on it, not a bar. */
|
|
24
|
+
readonly point: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The first day of a month, as a day. */
|
|
28
|
+
const firstDayOf = (month: string): string => `${month}-01`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The bars of the goals that touch the span of `months` from `from` on, cut to it: from the
|
|
32
|
+
* goal's start, or the span's first day when it began before, to its target day whole, or the
|
|
33
|
+
* span's end when it ends after. A goal with a start or a target alone sits on that day; a goal
|
|
34
|
+
* with neither is left out.
|
|
35
|
+
*/
|
|
36
|
+
export function timelineBars(
|
|
37
|
+
goals: readonly RestGoal[],
|
|
38
|
+
from: string,
|
|
39
|
+
months: number,
|
|
40
|
+
): TimelineBar[] {
|
|
41
|
+
const first = firstDayOf(from);
|
|
42
|
+
const end = firstDayOf(addMonths(from, months));
|
|
43
|
+
const total = daysBetween(first, end);
|
|
44
|
+
if (total <= 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return goals.flatMap((goal) => {
|
|
48
|
+
const startsOn = goal.startsOn ?? goal.dueOn;
|
|
49
|
+
const dueOn = goal.dueOn ?? goal.startsOn;
|
|
50
|
+
if (startsOn === null || dueOn === null || dueOn < first || startsOn >= end) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const begins = startsOn >= first;
|
|
54
|
+
const ends = dueOn < end;
|
|
55
|
+
const fromDay = begins ? daysBetween(first, startsOn) : 0;
|
|
56
|
+
const toDay = ends ? daysBetween(first, dueOn) + 1 : total;
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
goal,
|
|
60
|
+
from: fromDay / total,
|
|
61
|
+
to: toDay / total,
|
|
62
|
+
begins,
|
|
63
|
+
ends,
|
|
64
|
+
point: startsOn === dueOn,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GoalTimelineProps {
|
|
71
|
+
readonly goals: readonly RestGoal[];
|
|
72
|
+
/** The first month shown: `2026-10`. */
|
|
73
|
+
readonly from: string;
|
|
74
|
+
/** How many months are shown; `DEFAULT_TIMELINE_MONTHS` when left out. */
|
|
75
|
+
readonly months?: number | undefined;
|
|
76
|
+
/** The day to count from; the browser's today when left out. */
|
|
77
|
+
readonly today?: string | undefined;
|
|
78
|
+
/** The workspace's categories, for the legend to say what the colours mean. */
|
|
79
|
+
readonly categories?: readonly RestCategory[] | undefined;
|
|
80
|
+
readonly goalHref: (goal: RestGoal) => string;
|
|
81
|
+
/** Where the roadmap is from another month on. */
|
|
82
|
+
readonly fromHref: (month: string) => string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const percent = (share: number): string => `${(share * 100).toFixed(3)}%`;
|
|
86
|
+
|
|
87
|
+
export function GoalTimeline(props: GoalTimelineProps) {
|
|
88
|
+
const words = useWords().roadmap;
|
|
89
|
+
const { tag } = useLanguage();
|
|
90
|
+
const months = Math.max(1, props.months ?? DEFAULT_TIMELINE_MONTHS);
|
|
91
|
+
const now = props.today ?? todayOf();
|
|
92
|
+
const first = firstDayOf(props.from);
|
|
93
|
+
const end = firstDayOf(addMonths(props.from, months));
|
|
94
|
+
const total = daysBetween(first, end);
|
|
95
|
+
const columns = Array.from({ length: months }, (_, index) => addMonths(props.from, index));
|
|
96
|
+
const shares = columns.map(
|
|
97
|
+
(month) => daysBetween(firstDayOf(month), firstDayOf(addMonths(month, 1))) / total,
|
|
98
|
+
);
|
|
99
|
+
const bars = timelineBars(props.goals, props.from, months);
|
|
100
|
+
const todayAt = now >= first && now < end ? daysBetween(first, now) / total : undefined;
|
|
101
|
+
const undated = props.goals.filter((goal) => goal.startsOn === null && goal.dueOn === null);
|
|
102
|
+
const doneShare = (goal: RestGoal): number => {
|
|
103
|
+
const counts = goal.taskCounts;
|
|
104
|
+
const all = counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
|
|
105
|
+
return all === 0 ? 0 : counts.done / all;
|
|
106
|
+
};
|
|
107
|
+
return (
|
|
108
|
+
<div className="sc-timeline">
|
|
109
|
+
<div className="sc-timeline-head">
|
|
110
|
+
<h2 className="sc-timeline-title">{words.timeline.title}</h2>
|
|
111
|
+
<nav className="sc-calendar-nav" aria-label={words.timeline.title}>
|
|
112
|
+
<a
|
|
113
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
114
|
+
href={props.fromHref(addMonths(props.from, -1))}
|
|
115
|
+
aria-label={words.timeline.earlier}
|
|
116
|
+
>
|
|
117
|
+
{"<"}
|
|
118
|
+
</a>
|
|
119
|
+
<a className="sc-button sc-button-ghost sc-button-sm" href={props.fromHref(monthOf(now))}>
|
|
120
|
+
{words.timeline.thisMonth}
|
|
121
|
+
</a>
|
|
122
|
+
<a
|
|
123
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
124
|
+
href={props.fromHref(addMonths(props.from, 1))}
|
|
125
|
+
aria-label={words.timeline.later}
|
|
126
|
+
>
|
|
127
|
+
{">"}
|
|
128
|
+
</a>
|
|
129
|
+
</nav>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="sc-timeline-grid">
|
|
132
|
+
<div className="sc-timeline-row sc-timeline-months">
|
|
133
|
+
<div className="sc-timeline-label" aria-hidden="true" />
|
|
134
|
+
<div className="sc-timeline-track">
|
|
135
|
+
{columns.map((month, index) => (
|
|
136
|
+
<span
|
|
137
|
+
key={month}
|
|
138
|
+
className="sc-timeline-month"
|
|
139
|
+
style={{ width: percent(shares[index] ?? 0) }}
|
|
140
|
+
>
|
|
141
|
+
{formatMonth(month, tag)}
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
{todayAt !== undefined && (
|
|
145
|
+
<span
|
|
146
|
+
className="sc-timeline-today sc-timeline-today-mark"
|
|
147
|
+
style={{ left: percent(todayAt) }}
|
|
148
|
+
title={words.timeline.today}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
{bars.map(({ goal, from, to, begins, ends, point }) => {
|
|
154
|
+
const counts = goal.taskCounts;
|
|
155
|
+
const all =
|
|
156
|
+
counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
|
|
157
|
+
return (
|
|
158
|
+
<div key={goal.id} className={cx("sc-timeline-row", `sc-goal-${goal.state}`)}>
|
|
159
|
+
<div className="sc-timeline-label">
|
|
160
|
+
<a className="sc-timeline-goal" href={props.goalHref(goal)}>
|
|
161
|
+
{goal.title}
|
|
162
|
+
</a>
|
|
163
|
+
<GoalStateBadge state={goal.state} />
|
|
164
|
+
</div>
|
|
165
|
+
<div className="sc-timeline-track">
|
|
166
|
+
{columns.map((month, index) => (
|
|
167
|
+
<span
|
|
168
|
+
key={month}
|
|
169
|
+
className="sc-timeline-column"
|
|
170
|
+
style={{ width: percent(shares[index] ?? 0) }}
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
/>
|
|
173
|
+
))}
|
|
174
|
+
{todayAt !== undefined && (
|
|
175
|
+
<span
|
|
176
|
+
className="sc-timeline-today"
|
|
177
|
+
style={{ left: percent(todayAt) }}
|
|
178
|
+
aria-hidden="true"
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
<a
|
|
182
|
+
className={cx(
|
|
183
|
+
point ? "sc-timeline-point" : "sc-timeline-bar",
|
|
184
|
+
`sc-category-${goal.category?.color ?? "gray"}`,
|
|
185
|
+
begins && "sc-timeline-bar-begins",
|
|
186
|
+
ends && "sc-timeline-bar-ends",
|
|
187
|
+
)}
|
|
188
|
+
style={
|
|
189
|
+
point
|
|
190
|
+
? { left: percent(from) }
|
|
191
|
+
: { left: percent(from), width: percent(to - from) }
|
|
192
|
+
}
|
|
193
|
+
href={props.goalHref(goal)}
|
|
194
|
+
aria-label={`${goal.title}: ${words.progress(counts.done, all)}`}
|
|
195
|
+
>
|
|
196
|
+
{!point && (
|
|
197
|
+
<>
|
|
198
|
+
<span
|
|
199
|
+
className="sc-timeline-fill"
|
|
200
|
+
style={{ width: percent(doneShare(goal)) }}
|
|
201
|
+
aria-hidden="true"
|
|
202
|
+
/>
|
|
203
|
+
<span className="sc-timeline-words">
|
|
204
|
+
{counts.done}/{all}
|
|
205
|
+
</span>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</a>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
{bars.length === 0 && <p className="sc-muted">{words.timeline.nothing}</p>}
|
|
215
|
+
{(props.categories ?? []).length > 0 && (
|
|
216
|
+
<dl className="sc-calendar-legend" aria-label={words.timeline.legend}>
|
|
217
|
+
{(props.categories ?? []).map((category) => (
|
|
218
|
+
<div key={category.key}>
|
|
219
|
+
<dt>
|
|
220
|
+
<span className={cx("sc-calendar-dot", `sc-category-${category.color}`)} />
|
|
221
|
+
</dt>
|
|
222
|
+
<dd>{category.name}</dd>
|
|
223
|
+
</div>
|
|
224
|
+
))}
|
|
225
|
+
</dl>
|
|
226
|
+
)}
|
|
227
|
+
{undated.length > 0 && (
|
|
228
|
+
<p className="sc-timeline-undated">
|
|
229
|
+
<span className="sc-muted">{words.timeline.noDays}: </span>
|
|
230
|
+
{undated.map((goal, index) => (
|
|
231
|
+
<span key={goal.id}>
|
|
232
|
+
{index > 0 && ", "}
|
|
233
|
+
<a className="sc-timeline-goal" href={props.goalHref(goal)}>
|
|
234
|
+
{goal.title}
|
|
235
|
+
</a>
|
|
236
|
+
</span>
|
|
237
|
+
))}
|
|
238
|
+
</p>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -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
|
|
29
|
-
// about it
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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
|
|
54
|
-
readonly fileHref?: ((
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
193
|
+
decisions={about}
|
|
184
194
|
fileHref={props.fileHref}
|
|
195
|
+
decisionHref={props.decisionHref}
|
|
185
196
|
docHref={props.docHref}
|
|
186
|
-
|
|
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>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
type ClientVerdict,
|
|
3
4
|
DEFAULT_TOKEN_DAYS,
|
|
5
|
+
judgeClient,
|
|
4
6
|
MAX_TOKEN_NAME_LENGTH,
|
|
7
|
+
parseVersion,
|
|
5
8
|
type RestToken,
|
|
6
9
|
type RestTokenInput,
|
|
7
10
|
} from "../api.js";
|
|
8
11
|
import { useWords } from "../i18n/index.js";
|
|
9
|
-
import { Button, Callout, CopyButton, Time } from "./ui.js";
|
|
12
|
+
import { Badge, Button, Callout, CopyButton, Time } from "./ui.js";
|
|
10
13
|
|
|
11
14
|
// The tokens a person holds for their agents, scripts and consoles of their own, shown as the
|
|
12
15
|
// agents that hold them: the list, the way to make one by hand, and the one just made, whose
|
|
@@ -36,6 +39,22 @@ export interface TokenListProps {
|
|
|
36
39
|
readonly onRevoke?: ((token: RestToken) => void) | undefined;
|
|
37
40
|
readonly busy?: boolean | undefined;
|
|
38
41
|
readonly empty?: ReactNode;
|
|
42
|
+
/** The console's version (ADR-0025), so that the version of the command an agent runs is said to be behind or ahead; left out, the version alone is shown. */
|
|
43
|
+
readonly consoleVersion?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What a command's version is to the console's: nothing to say when they go together, else which way they differ. */
|
|
47
|
+
function verdictOf(
|
|
48
|
+
client: string,
|
|
49
|
+
consoleVersion: string | undefined,
|
|
50
|
+
): Exclude<ClientVerdict, "current"> | undefined {
|
|
51
|
+
const command = parseVersion(client);
|
|
52
|
+
const server = parseVersion(consoleVersion);
|
|
53
|
+
if (command === undefined || server === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const { verdict } = judgeClient(command, server);
|
|
57
|
+
return verdict === "current" ? undefined : verdict;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
export function TokenList(props: TokenListProps) {
|
|
@@ -53,6 +72,24 @@ export function TokenList(props: TokenListProps) {
|
|
|
53
72
|
<span>
|
|
54
73
|
{words.connected} <Time iso={token.createdAt} />
|
|
55
74
|
</span>
|
|
75
|
+
{token.client !== null && (
|
|
76
|
+
<span>
|
|
77
|
+
{words.command(token.client)}
|
|
78
|
+
{((verdict) =>
|
|
79
|
+
verdict === undefined ? null : (
|
|
80
|
+
<>
|
|
81
|
+
{" "}
|
|
82
|
+
<Badge
|
|
83
|
+
tone={
|
|
84
|
+
verdict === "outdated" || verdict === "too_new" ? "danger" : "warning"
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
{words.commandVerdict[verdict]}
|
|
88
|
+
</Badge>
|
|
89
|
+
</>
|
|
90
|
+
))(verdictOf(token.client, props.consoleVersion))}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
56
93
|
<span>
|
|
57
94
|
{token.expiresAt === null ? (
|
|
58
95
|
words.doesNotExpire
|
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,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
type ProjectListProps,
|
|
53
54
|
ProjectMark,
|
|
54
55
|
} from "./components/projects.js";
|
|
56
|
+
import { GoalTimeline } from "./components/roadmap.js";
|
|
55
57
|
import { RulesForm, RulesView } from "./components/rules.js";
|
|
56
58
|
import {
|
|
57
59
|
type Brand,
|
|
@@ -105,6 +107,7 @@ import {
|
|
|
105
107
|
projectHref,
|
|
106
108
|
projectSettingsHref,
|
|
107
109
|
type Route,
|
|
110
|
+
roadmapHref,
|
|
108
111
|
settingsHref,
|
|
109
112
|
signInFailureOf,
|
|
110
113
|
taskHref,
|
|
@@ -457,7 +460,8 @@ function TaskPage(props: {
|
|
|
457
460
|
? data.tasks.find((candidate) => candidate.id === route.id)
|
|
458
461
|
: data.tasks.find((candidate) => candidate.number === route.number);
|
|
459
462
|
const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
|
|
460
|
-
const
|
|
463
|
+
const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
|
|
464
|
+
const { taskHistory, taskComments } = data.actions;
|
|
461
465
|
const taskId = task?.id;
|
|
462
466
|
const number = task?.number;
|
|
463
467
|
// A task named by its id is shown at its number's address.
|
|
@@ -481,8 +485,15 @@ function TaskPage(props: {
|
|
|
481
485
|
}
|
|
482
486
|
})
|
|
483
487
|
.catch(() => undefined);
|
|
488
|
+
taskComments(taskId, controller.signal)
|
|
489
|
+
.then((items) => {
|
|
490
|
+
if (!controller.signal.aborted) {
|
|
491
|
+
setComments(items);
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
.catch(() => undefined);
|
|
484
495
|
return () => controller.abort();
|
|
485
|
-
}, [taskId, taskHistory, feedLength]);
|
|
496
|
+
}, [taskId, taskHistory, taskComments, feedLength]);
|
|
486
497
|
if (task === undefined) {
|
|
487
498
|
return (
|
|
488
499
|
<EmptyState
|
|
@@ -500,13 +511,14 @@ function TaskPage(props: {
|
|
|
500
511
|
tasks={data.tasks}
|
|
501
512
|
decisions={data.decisions}
|
|
502
513
|
runs={data.runs}
|
|
514
|
+
comments={comments}
|
|
503
515
|
history={history}
|
|
504
516
|
categories={data.categories}
|
|
505
517
|
goals={data.goals}
|
|
506
518
|
taskHref={(candidate) => taskHref(key, candidate.number)}
|
|
507
519
|
decisionHref={(decisionId) => decisionHref(key, decisionId)}
|
|
508
520
|
goalHref={(goalId) => goalHref(key, goalId)}
|
|
509
|
-
fileHref={(
|
|
521
|
+
fileHref={(fileId) => projectPath(key, "files", fileId)}
|
|
510
522
|
docHref={(path) => docHref(key, path)}
|
|
511
523
|
form={route.form}
|
|
512
524
|
formHref={(form) => taskHref(key, task.number, form)}
|
|
@@ -533,6 +545,19 @@ function TaskPage(props: {
|
|
|
533
545
|
void act(() => data.actions.updateDecision(decision.id, patch))
|
|
534
546
|
}
|
|
535
547
|
onAbandonRun={(run) => void act(() => data.actions.abandonRun(run.id))}
|
|
548
|
+
onComment={(draft) =>
|
|
549
|
+
void act(async () => {
|
|
550
|
+
await data.actions.comment(task.id, {
|
|
551
|
+
body: draft.body,
|
|
552
|
+
files: draft.files.map((file) => ({
|
|
553
|
+
name: file.name,
|
|
554
|
+
bytes: file,
|
|
555
|
+
...(file.type === "" ? {} : { contentType: file.type }),
|
|
556
|
+
})),
|
|
557
|
+
});
|
|
558
|
+
setComments(await taskComments(task.id));
|
|
559
|
+
})
|
|
560
|
+
}
|
|
536
561
|
/>
|
|
537
562
|
);
|
|
538
563
|
}
|
|
@@ -679,6 +704,7 @@ function ProjectPage(props: {
|
|
|
679
704
|
categories={data.categories}
|
|
680
705
|
goals={data.goals}
|
|
681
706
|
taskHref={href}
|
|
707
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
682
708
|
monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
|
|
683
709
|
/>
|
|
684
710
|
<div className="sc-calendar-below">
|
|
@@ -742,6 +768,7 @@ function ProjectPage(props: {
|
|
|
742
768
|
{making && (
|
|
743
769
|
<section className="sc-panel sc-panel-form" aria-label={roadmap.setGoalAria}>
|
|
744
770
|
<GoalForm
|
|
771
|
+
categories={data.categories}
|
|
745
772
|
busy={busy}
|
|
746
773
|
onSubmit={(input) =>
|
|
747
774
|
void act(async () => {
|
|
@@ -753,6 +780,18 @@ function ProjectPage(props: {
|
|
|
753
780
|
/>
|
|
754
781
|
</section>
|
|
755
782
|
)}
|
|
783
|
+
{data.goals.length > 0 && (
|
|
784
|
+
<GoalTimeline
|
|
785
|
+
goals={data.goals}
|
|
786
|
+
from={
|
|
787
|
+
route.name === "roadmap" && route.from !== undefined ? route.from : monthOf(today())
|
|
788
|
+
}
|
|
789
|
+
today={today()}
|
|
790
|
+
categories={data.categories}
|
|
791
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
792
|
+
fromHref={(month) => roadmapHref(key, month)}
|
|
793
|
+
/>
|
|
794
|
+
)}
|
|
756
795
|
<components.GoalList
|
|
757
796
|
goals={data.goals}
|
|
758
797
|
goalHref={(goal) => goalHref(key, goal.id)}
|
|
@@ -797,6 +836,7 @@ function ProjectPage(props: {
|
|
|
797
836
|
<section className="sc-panel sc-panel-form" aria-label={roadmap.editAria}>
|
|
798
837
|
<GoalForm
|
|
799
838
|
goal={goal}
|
|
839
|
+
categories={data.categories}
|
|
800
840
|
busy={busy}
|
|
801
841
|
onSubmit={(input) =>
|
|
802
842
|
void act(async () => {
|
|
@@ -1428,6 +1468,7 @@ function PersonAgentsPage(props: {
|
|
|
1428
1468
|
<components.TokenList
|
|
1429
1469
|
tokens={tokens}
|
|
1430
1470
|
busy={busy}
|
|
1471
|
+
consoleVersion={data.me?.console?.version}
|
|
1431
1472
|
onRevoke={(token) =>
|
|
1432
1473
|
void act(async () => {
|
|
1433
1474
|
await data.actions.revokePersonToken(id, token.id);
|
|
@@ -1740,6 +1781,7 @@ function WorkspacePage(props: {
|
|
|
1740
1781
|
<components.TokenList
|
|
1741
1782
|
tokens={data.tokens}
|
|
1742
1783
|
busy={busy}
|
|
1784
|
+
consoleVersion={data.me?.console?.version}
|
|
1743
1785
|
onRevoke={(token) => void act(() => data.actions.revokeToken(token.id))}
|
|
1744
1786
|
empty={<EmptyState title={agents.empty.title} body={agents.empty.body} />}
|
|
1745
1787
|
/>
|
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>;
|
|
@@ -261,7 +267,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
261
267
|
scope.get(signal),
|
|
262
268
|
scope.tasks(undefined, signal),
|
|
263
269
|
scope.decisions(undefined, signal),
|
|
264
|
-
scope.goals(signal),
|
|
270
|
+
scope.goals({}, signal),
|
|
265
271
|
scope.runs(undefined, signal),
|
|
266
272
|
scope.members(signal),
|
|
267
273
|
]);
|
|
@@ -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);
|