@skillcdn/console 0.1.20 → 0.1.22
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 +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +67 -6
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +15 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +133 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts +8 -4
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +67 -19
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +13 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -1
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +4 -4
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +48 -19
- package/dist/components/board.js.map +1 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/console.css +59 -17
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +1 -1
- package/dist/console.js.map +1 -1
- package/dist/i18n/en.js +1 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.js +1 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +3 -2
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +145 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +89 -0
- package/dist/schemas.js.map +1 -1
- package/docs/specs/cli.md +5 -5
- package/docs/specs/rest.md +8 -3
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +4 -4
- package/src/api.ts +8 -0
- package/src/cli/cli.ts +77 -9
- package/src/cli/format.ts +156 -7
- package/src/cli/help.ts +93 -25
- package/src/client.ts +48 -0
- package/src/components/board.tsx +86 -33
- package/src/components/task-view.tsx +10 -12
- package/src/console.tsx +8 -2
- package/src/i18n/en.ts +1 -1
- package/src/i18n/ko.ts +1 -1
- package/src/rules.ts +3 -2
- package/src/schemas.ts +110 -0
- package/src/styles/console.css +59 -17
package/src/client.ts
CHANGED
|
@@ -61,7 +61,9 @@ import {
|
|
|
61
61
|
type RestRunEndInput,
|
|
62
62
|
type RestRunInput,
|
|
63
63
|
type RestRuns,
|
|
64
|
+
type RestSkillBrowse,
|
|
64
65
|
type RestSkillFile,
|
|
66
|
+
type RestSkillFind,
|
|
65
67
|
type RestSkillLoad,
|
|
66
68
|
type RestSkills,
|
|
67
69
|
type RestTask,
|
|
@@ -103,7 +105,9 @@ import {
|
|
|
103
105
|
restRulesSchema,
|
|
104
106
|
restRunSchema,
|
|
105
107
|
restRunsSchema,
|
|
108
|
+
restSkillBrowseSchema,
|
|
106
109
|
restSkillFileSchema,
|
|
110
|
+
restSkillFindSchema,
|
|
107
111
|
restSkillLoadSchema,
|
|
108
112
|
restSkillsSchema,
|
|
109
113
|
restTaskSchema,
|
|
@@ -318,6 +322,25 @@ export interface ProjectClient {
|
|
|
318
322
|
page?: { readonly offset?: number | undefined; readonly limit?: number | undefined },
|
|
319
323
|
signal?: AbortSignal,
|
|
320
324
|
): Promise<RestSkillFile>;
|
|
325
|
+
/** What a folder of the pack holds, a page of it (`browse_repo`): the mounted folder when `path` is left out. */
|
|
326
|
+
skillBrowse(
|
|
327
|
+
page?: {
|
|
328
|
+
readonly path?: string | undefined;
|
|
329
|
+
readonly cursor?: string | undefined;
|
|
330
|
+
readonly limit?: number | undefined;
|
|
331
|
+
},
|
|
332
|
+
signal?: AbortSignal,
|
|
333
|
+
): Promise<RestSkillBrowse>;
|
|
334
|
+
/** The skills and the documents of the pack that words find, a page of them in relevance order (`search_repo`), within `path` when given. */
|
|
335
|
+
skillFind(
|
|
336
|
+
query: string,
|
|
337
|
+
options?: {
|
|
338
|
+
readonly path?: string | undefined;
|
|
339
|
+
readonly cursor?: string | undefined;
|
|
340
|
+
readonly limit?: number | undefined;
|
|
341
|
+
},
|
|
342
|
+
signal?: AbortSignal,
|
|
343
|
+
): Promise<RestSkillFind>;
|
|
321
344
|
/** Sets the pack token the project reads a private pack with; for an owner. The token travels once and is answered never. */
|
|
322
345
|
setPackToken(input: RestPackTokenInput): Promise<RestProject>;
|
|
323
346
|
/** Takes the pack token away; for an owner. */
|
|
@@ -714,6 +737,31 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
714
737
|
restSkillFileSchema,
|
|
715
738
|
signal,
|
|
716
739
|
),
|
|
740
|
+
skillBrowse: (page = {}, signal) =>
|
|
741
|
+
request(
|
|
742
|
+
"GET",
|
|
743
|
+
withQuery(`${at("skills")}/browse`, {
|
|
744
|
+
path: page.path,
|
|
745
|
+
cursor: page.cursor,
|
|
746
|
+
limit: page.limit === undefined ? undefined : String(page.limit),
|
|
747
|
+
}),
|
|
748
|
+
undefined,
|
|
749
|
+
restSkillBrowseSchema,
|
|
750
|
+
signal,
|
|
751
|
+
),
|
|
752
|
+
skillFind: (query, options = {}, signal) =>
|
|
753
|
+
request(
|
|
754
|
+
"GET",
|
|
755
|
+
withQuery(`${at("skills")}/find`, {
|
|
756
|
+
query,
|
|
757
|
+
path: options.path,
|
|
758
|
+
cursor: options.cursor,
|
|
759
|
+
limit: options.limit === undefined ? undefined : String(options.limit),
|
|
760
|
+
}),
|
|
761
|
+
undefined,
|
|
762
|
+
restSkillFindSchema,
|
|
763
|
+
signal,
|
|
764
|
+
),
|
|
717
765
|
setPackToken: (input) =>
|
|
718
766
|
request("PUT", `${base}${at()}/pack-token`, input, restProjectSchema),
|
|
719
767
|
removePackToken: () =>
|
package/src/components/board.tsx
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
} from "../api.js";
|
|
19
19
|
import { useWords } from "../i18n/index.js";
|
|
20
20
|
import type { BoardFilter } from "../router.js";
|
|
21
|
-
import { OPEN_TASK_STATES } from "../vocabulary.js";
|
|
22
21
|
import { isPlainClick } from "./shell.js";
|
|
23
22
|
import {
|
|
24
23
|
Avatar,
|
|
@@ -35,12 +34,13 @@ import {
|
|
|
35
34
|
// A card says what a person needs at a glance and nothing more: the number and the title, the
|
|
36
35
|
// category, what is due, who is on it, and what blocks it. Whether an agent is at it is not
|
|
37
36
|
// said, since the board cannot know (ADR-0020); the task's page shows its runs. A card is moved
|
|
38
|
-
// to another column by hand: pressed anywhere but on its
|
|
39
|
-
// a mouse at once and under a finger once it has rested a moment, so that a finger
|
|
40
|
-
// at once scrolls as it always did
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// what it is told on a click goes
|
|
37
|
+
// to another column by hand: pressed anywhere but on its menu, its title included, then dragged
|
|
38
|
+
// whole, with a mouse at once and under a finger once it has rested a moment, so that a finger
|
|
39
|
+
// that moves at once scrolls as it always did; a click on the title opens the task as long as the
|
|
40
|
+
// card was not dragged. The card's menu holds the rest: the way to the task's form, and the way to
|
|
41
|
+
// put the task away (ADR-0029, ADR-0030). The keyboard changes a state on the task's page. The
|
|
42
|
+
// board takes its data as props and nothing from the network; what it is told on a click goes
|
|
43
|
+
// back up.
|
|
44
44
|
|
|
45
45
|
export interface BoardProps {
|
|
46
46
|
readonly tasks: readonly RestTask[];
|
|
@@ -57,7 +57,7 @@ export interface BoardProps {
|
|
|
57
57
|
readonly editHref?: ((task: RestTask) => string) | undefined;
|
|
58
58
|
/** Called when the menu's edit is chosen, to show the form without leaving the page; left out, the link is followed. */
|
|
59
59
|
readonly onEdit?: ((task: RestTask) => void) | undefined;
|
|
60
|
-
/** Called when a task
|
|
60
|
+
/** Called when a task is put away from its card's menu (ADR-0029, ADR-0030). Left out, the menu offers nothing of it. */
|
|
61
61
|
readonly onArchive?: ((task: RestTask) => void) | undefined;
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -78,12 +78,21 @@ export function columnStateOf(value: string | null | undefined): TaskState | und
|
|
|
78
78
|
return TASK_STATES.find((state) => state === value);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
interface Point {
|
|
82
|
+
readonly x: number;
|
|
83
|
+
readonly y: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
/** A card on its way to another column. */
|
|
82
87
|
interface Drag {
|
|
83
88
|
readonly task: RestTask;
|
|
84
|
-
readonly from:
|
|
89
|
+
readonly from: Point;
|
|
85
90
|
readonly x: number;
|
|
86
91
|
readonly y: number;
|
|
92
|
+
/** Where on the card it was taken hold of, so that the card moves whole under the pointer. */
|
|
93
|
+
readonly grip: Point;
|
|
94
|
+
/** How wide the card is where it stands, so that the one in hand is the same. */
|
|
95
|
+
readonly width: number;
|
|
87
96
|
/** Whether the card has come loose: the mouse has travelled, or the finger has rested. */
|
|
88
97
|
readonly moving: boolean;
|
|
89
98
|
/** Whether the press is a finger's, which must rest before the card comes loose. */
|
|
@@ -97,23 +106,21 @@ export interface TaskCardProps {
|
|
|
97
106
|
readonly href: string;
|
|
98
107
|
readonly onOpen: (task: RestTask) => void;
|
|
99
108
|
readonly today?: string | undefined;
|
|
100
|
-
/** Called when the card is pressed anywhere but on its
|
|
109
|
+
/** Called when the card is pressed anywhere but on its menu, to drag it to another column; left out, the card stays where it is. */
|
|
101
110
|
readonly onPress?: ((task: RestTask, event: ReactPointerEvent<HTMLElement>) => void) | undefined;
|
|
102
|
-
/** Whether the card is on its way to another column. */
|
|
111
|
+
/** Whether the card is on its way to another column: the one left in its place is faded. */
|
|
103
112
|
readonly dragging?: boolean | undefined;
|
|
104
113
|
/** Where the task is edited; given, the card's menu leads there. */
|
|
105
114
|
readonly editHref?: string | undefined;
|
|
106
115
|
/** Called when the menu's edit is chosen, to show the form without leaving the page; left out, the link is followed. */
|
|
107
116
|
readonly onEdit?: ((task: RestTask) => void) | undefined;
|
|
108
|
-
/** Called when the task is put away from the card's menu
|
|
117
|
+
/** Called when the task is put away from the card's menu, whatever its state (ADR-0030). */
|
|
109
118
|
readonly onArchive?: ((task: RestTask) => void) | undefined;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
export function TaskCard(props: TaskCardProps) {
|
|
113
122
|
const words = useWords();
|
|
114
123
|
const { task, onPress } = props;
|
|
115
|
-
const closed = !OPEN_TASK_STATES.includes(task.state);
|
|
116
|
-
const archive = closed ? props.onArchive : undefined;
|
|
117
124
|
return (
|
|
118
125
|
<article
|
|
119
126
|
className={cx(
|
|
@@ -124,12 +131,19 @@ export function TaskCard(props: TaskCardProps) {
|
|
|
124
131
|
aria-label={`#${task.number} ${task.title}`}
|
|
125
132
|
onPointerDown={onPress === undefined ? undefined : (event) => onPress(task, event)}
|
|
126
133
|
>
|
|
127
|
-
{(props.editHref !== undefined ||
|
|
128
|
-
<CardMenu
|
|
134
|
+
{(props.editHref !== undefined || props.onArchive !== undefined) && (
|
|
135
|
+
<CardMenu
|
|
136
|
+
task={task}
|
|
137
|
+
editHref={props.editHref}
|
|
138
|
+
onEdit={props.onEdit}
|
|
139
|
+
onArchive={props.onArchive}
|
|
140
|
+
/>
|
|
129
141
|
)}
|
|
130
142
|
<a
|
|
131
143
|
className="sc-card-title"
|
|
132
144
|
href={props.href}
|
|
145
|
+
// The card is dragged by our own hand, the title included; the browser's drag of a link is not it.
|
|
146
|
+
draggable={false}
|
|
133
147
|
onClick={(event) => {
|
|
134
148
|
if (isPlainClick(event)) {
|
|
135
149
|
event.preventDefault();
|
|
@@ -169,7 +183,8 @@ const MENU_MARGIN = 8;
|
|
|
169
183
|
/**
|
|
170
184
|
* The card's menu: three dots in its corner, which open the ways a card has beyond being moved,
|
|
171
185
|
* each given by the composition. The menu is rendered over the document, so that no column
|
|
172
|
-
* clips it; a press elsewhere, the escape key, a scroll or a change of the window closes it
|
|
186
|
+
* clips it; a press elsewhere, the escape key, a scroll or a change of the window closes it, and
|
|
187
|
+
* so does the button that opened it.
|
|
173
188
|
*/
|
|
174
189
|
function CardMenu(props: {
|
|
175
190
|
readonly task: RestTask;
|
|
@@ -194,6 +209,13 @@ function CardMenu(props: {
|
|
|
194
209
|
right: Math.max(MENU_MARGIN, window.innerWidth - rect.right),
|
|
195
210
|
});
|
|
196
211
|
};
|
|
212
|
+
/** Whether the focus, or a press, is on the menu or on its button: the menu's own. */
|
|
213
|
+
const own = useCallback(
|
|
214
|
+
(target: EventTarget | null): boolean =>
|
|
215
|
+
target instanceof Node &&
|
|
216
|
+
(panel.current?.contains(target) === true || button.current?.contains(target) === true),
|
|
217
|
+
[],
|
|
218
|
+
);
|
|
197
219
|
useEffect(() => {
|
|
198
220
|
if (!open) {
|
|
199
221
|
return;
|
|
@@ -214,14 +236,9 @@ function CardMenu(props: {
|
|
|
214
236
|
}
|
|
215
237
|
panel.current?.querySelector<HTMLElement>("[role='menuitem']")?.focus({ preventScroll: true });
|
|
216
238
|
const away = (event: PointerEvent) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
target instanceof Node &&
|
|
220
|
-
(panel.current?.contains(target) === true || button.current?.contains(target) === true)
|
|
221
|
-
) {
|
|
222
|
-
return;
|
|
239
|
+
if (!own(event.target)) {
|
|
240
|
+
close();
|
|
223
241
|
}
|
|
224
|
-
close();
|
|
225
242
|
};
|
|
226
243
|
const key = (event: KeyboardEvent) => {
|
|
227
244
|
if (event.key === "Escape") {
|
|
@@ -239,7 +256,7 @@ function CardMenu(props: {
|
|
|
239
256
|
window.removeEventListener("scroll", close, true);
|
|
240
257
|
window.removeEventListener("resize", close);
|
|
241
258
|
};
|
|
242
|
-
}, [open, close]);
|
|
259
|
+
}, [open, close, own]);
|
|
243
260
|
// The arrow keys walk the items, around the ends.
|
|
244
261
|
const walk = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
245
262
|
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") {
|
|
@@ -283,9 +300,9 @@ function CardMenu(props: {
|
|
|
283
300
|
aria-label={`#${task.number} ${task.title}`}
|
|
284
301
|
style={{ top: place.top, right: place.right }}
|
|
285
302
|
onKeyDown={walk}
|
|
303
|
+
// The focus leaving for anywhere but the menu's own button closes it; the button closes it itself.
|
|
286
304
|
onBlur={(event) => {
|
|
287
|
-
|
|
288
|
-
if (!(next instanceof Node && panel.current?.contains(next) === true)) {
|
|
305
|
+
if (!own(event.relatedTarget)) {
|
|
289
306
|
close();
|
|
290
307
|
}
|
|
291
308
|
}}
|
|
@@ -340,6 +357,8 @@ export function byDue(a: RestTask, b: RestTask): number {
|
|
|
340
357
|
return b.number - a.number;
|
|
341
358
|
}
|
|
342
359
|
|
|
360
|
+
const nothing = () => undefined;
|
|
361
|
+
|
|
343
362
|
export function Board(props: BoardProps) {
|
|
344
363
|
const words = useWords();
|
|
345
364
|
const { onMove } = props;
|
|
@@ -353,10 +372,10 @@ export function Board(props: BoardProps) {
|
|
|
353
372
|
const loose = useRef(false);
|
|
354
373
|
|
|
355
374
|
const press = useCallback((task: RestTask, event: ReactPointerEvent<HTMLElement>) => {
|
|
356
|
-
// The card's
|
|
375
|
+
// The card's menu is pressed for itself; everything else on the card takes hold of it.
|
|
357
376
|
if (
|
|
358
377
|
event.button !== 0 ||
|
|
359
|
-
(event.target instanceof Element && event.target.closest("
|
|
378
|
+
(event.target instanceof Element && event.target.closest("button") !== null)
|
|
360
379
|
) {
|
|
361
380
|
return;
|
|
362
381
|
}
|
|
@@ -365,14 +384,26 @@ export function Board(props: BoardProps) {
|
|
|
365
384
|
// The press is the drag's beginning and nothing else: no text is selected on the way.
|
|
366
385
|
event.preventDefault();
|
|
367
386
|
}
|
|
387
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
368
388
|
const from = { x: event.clientX, y: event.clientY };
|
|
369
|
-
setDrag({
|
|
389
|
+
setDrag({
|
|
390
|
+
task,
|
|
391
|
+
from,
|
|
392
|
+
x: from.x,
|
|
393
|
+
y: from.y,
|
|
394
|
+
grip: { x: from.x - rect.left, y: from.y - rect.top },
|
|
395
|
+
width: rect.width,
|
|
396
|
+
moving: false,
|
|
397
|
+
hold,
|
|
398
|
+
over: undefined,
|
|
399
|
+
});
|
|
370
400
|
}, []);
|
|
371
401
|
|
|
372
402
|
// While a card is held, the pointer is followed over the whole document: the column under it
|
|
373
403
|
// is marked, the board scrolls when the pointer nears its edge, and the release moves the task.
|
|
374
404
|
// A finger's hold is timed here, and once the card has come loose the page no longer scrolls
|
|
375
|
-
// under the finger, nor asks for the menu a long press would otherwise bring up
|
|
405
|
+
// under the finger, nor asks for the menu a long press would otherwise bring up; the click that
|
|
406
|
+
// follows a drag opens nothing.
|
|
376
407
|
const held = drag !== null;
|
|
377
408
|
useEffect(() => {
|
|
378
409
|
if (!held) {
|
|
@@ -421,8 +452,18 @@ export function Board(props: BoardProps) {
|
|
|
421
452
|
return { ...before, moving: true, over: columnUnder(before.x, before.y) };
|
|
422
453
|
});
|
|
423
454
|
};
|
|
455
|
+
const swallow = (event: MouseEvent) => {
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
event.stopPropagation();
|
|
458
|
+
};
|
|
424
459
|
const release = (event: PointerEvent) => {
|
|
425
460
|
const last = current.current;
|
|
461
|
+
if (loose.current) {
|
|
462
|
+
// The click the browser sends after the pointer is let go would open the title's task:
|
|
463
|
+
// it is swallowed, and the ear for it taken down once that moment has passed.
|
|
464
|
+
window.addEventListener("click", swallow, true);
|
|
465
|
+
window.setTimeout(() => window.removeEventListener("click", swallow, true), 0);
|
|
466
|
+
}
|
|
426
467
|
if (last?.moving === true && event.type === "pointerup") {
|
|
427
468
|
const over = columnUnder(event.clientX, event.clientY);
|
|
428
469
|
if (over !== undefined && over !== last.task.state) {
|
|
@@ -506,12 +547,24 @@ export function Board(props: BoardProps) {
|
|
|
506
547
|
);
|
|
507
548
|
})}
|
|
508
549
|
{dragging !== null && (
|
|
550
|
+
// The card in hand: the whole of it, as wide as where it stands, under the pointer where it was taken hold of.
|
|
509
551
|
<div
|
|
510
552
|
className="sc-drag-ghost"
|
|
511
553
|
aria-hidden="true"
|
|
512
|
-
style={{
|
|
554
|
+
style={{
|
|
555
|
+
width: dragging.width,
|
|
556
|
+
transform: `translate(${dragging.x - dragging.grip.x}px, ${dragging.y - dragging.grip.y}px)`,
|
|
557
|
+
}}
|
|
513
558
|
>
|
|
514
|
-
<
|
|
559
|
+
<TaskCard
|
|
560
|
+
task={dragging.task}
|
|
561
|
+
href={props.taskHref(dragging.task)}
|
|
562
|
+
onOpen={nothing}
|
|
563
|
+
today={props.today}
|
|
564
|
+
// The menu too, inert under the pointer, so that the card in hand keeps the shape of the one in its place.
|
|
565
|
+
editHref={props.editHref?.(dragging.task)}
|
|
566
|
+
onArchive={props.onArchive}
|
|
567
|
+
/>
|
|
515
568
|
</div>
|
|
516
569
|
)}
|
|
517
570
|
</div>
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
import { formatDay } from "../dates.js";
|
|
17
17
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
18
18
|
import type { TaskFormKind } from "../router.js";
|
|
19
|
-
import {
|
|
19
|
+
import { TASK_STATES } from "../vocabulary.js";
|
|
20
20
|
import { type CommentDraft, TaskThread } from "./comments.js";
|
|
21
21
|
import { DecisionForm } from "./decision-form.js";
|
|
22
22
|
import { EventFeed } from "./event-feed.js";
|
|
@@ -354,17 +354,15 @@ export function TaskView(props: TaskViewProps) {
|
|
|
354
354
|
<a className="sc-button sc-button-secondary" href={props.formHref("ask")}>
|
|
355
355
|
{words.task.raiseDecision}
|
|
356
356
|
</a>
|
|
357
|
-
{props.onArchive !== undefined &&
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
</Button>
|
|
367
|
-
)}
|
|
357
|
+
{props.onArchive !== undefined && task.archivedAt === null && (
|
|
358
|
+
<Button
|
|
359
|
+
variant="secondary"
|
|
360
|
+
disabled={props.busy === true}
|
|
361
|
+
onClick={() => props.onArchive?.(task)}
|
|
362
|
+
>
|
|
363
|
+
{words.task.archive}
|
|
364
|
+
</Button>
|
|
365
|
+
)}
|
|
368
366
|
{props.onRestore !== undefined && task.archivedAt !== null && (
|
|
369
367
|
<Button
|
|
370
368
|
variant="secondary"
|
package/src/console.tsx
CHANGED
|
@@ -738,7 +738,14 @@ function ProjectPage(props: {
|
|
|
738
738
|
return (
|
|
739
739
|
<>
|
|
740
740
|
<div className="sc-page-head">
|
|
741
|
-
<
|
|
741
|
+
<div className="sc-page-heading">
|
|
742
|
+
<h1 className="sc-page-title">{project.name}</h1>
|
|
743
|
+
{project.description.length > 0 && (
|
|
744
|
+
<p className="sc-page-lead" title={project.description}>
|
|
745
|
+
{project.description}
|
|
746
|
+
</p>
|
|
747
|
+
)}
|
|
748
|
+
</div>
|
|
742
749
|
<div className="sc-page-actions">
|
|
743
750
|
<LinkButton href={projectHref(key, "archive")}>{words.board.archive}</LinkButton>
|
|
744
751
|
<LinkButton href={newTaskHref(key)} variant="primary">
|
|
@@ -746,7 +753,6 @@ function ProjectPage(props: {
|
|
|
746
753
|
</LinkButton>
|
|
747
754
|
</div>
|
|
748
755
|
</div>
|
|
749
|
-
{project.description.length > 0 && <p className="sc-lead">{project.description}</p>}
|
|
750
756
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
751
757
|
{data.tasks.length > 0 && (
|
|
752
758
|
<BoardFilters
|
package/src/i18n/en.ts
CHANGED
|
@@ -605,7 +605,7 @@ export const en = {
|
|
|
605
605
|
loading: "Loading the archive",
|
|
606
606
|
empty: {
|
|
607
607
|
title: "Nothing is put away",
|
|
608
|
-
body: "A task
|
|
608
|
+
body: "A task is put away from its page or its card, with its goal once the goal is closed, or by the console a while after it was done or dropped.",
|
|
609
609
|
},
|
|
610
610
|
since: "put away",
|
|
611
611
|
restore: "Bring back",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -569,7 +569,7 @@ export const ko: Messages = {
|
|
|
569
569
|
loading: "보관함을 불러오는 중",
|
|
570
570
|
empty: {
|
|
571
571
|
title: "보관된 작업이 없습니다",
|
|
572
|
-
body: "
|
|
572
|
+
body: "작업은 작업 페이지나 카드 메뉴에서, 목표를 닫을 때 함께, 또는 완료·폐기된 뒤 일정 기간이 지나면 콘솔이 보관합니다.",
|
|
573
573
|
},
|
|
574
574
|
since: "보관",
|
|
575
575
|
restore: "되살리기",
|
package/src/rules.ts
CHANGED
|
@@ -23,12 +23,13 @@ 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, the runs on it and the discussion on it, without taking it. A task put away (archived)
|
|
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. A task put away (archived) is out of the listings and the board, and opens by its number still. How the board is worked is the skill working-the-board, shipped with the command: read it before the first command of a run (`console skills` prints 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
|
+
'3. Work given in chat, with no task on the board yet, goes on the board before it is done: write it (`console task new "<title>" --body <markdown>`), tell the person its number, take it (`console take <number>`) and work it as any task, without asking whether to. Until the task is written, the board is only read.',
|
|
28
29
|
"",
|
|
29
30
|
"## The organization's skills",
|
|
30
31
|
"",
|
|
31
|
-
"`console skills` lists the skills of this project's pack: each with its name, what it does, and the path of its SKILL.md. `console skill <path>` reads one whole, its rules first, as the console reads it from the SkillCDN deployment for you, a private pack with the pack token the project holds; `console skill
|
|
32
|
+
"`console skills` lists the skills of this project's pack: each with its name, what it does, and the path of its SKILL.md; `console skills <folder>` shows what a folder of the pack holds, and `console skills --search <words>` finds its skills and documents by words. `console skill <path>` reads one whole, its rules first, as the console reads it from the SkillCDN deployment for you, a private pack with the pack token the project holds; `console skill --file <file>` reads a file of the pack. Where your host lists skills through a connection of your own to the address, the URI `skill://gh/<owner>/<repo>/<path>` is the skill there, followed until its context is complete. Either way, never fetch a skill another way. Load the skill the task names before anything is made; with none named, the one whose description fits the work. The rules those skills share arrive with each of them, from their repository's SKILLCDN.md, and hold within that work. What you read of a private pack is the project's: it stays in your work, and is not copied into a report, a page or a comment beyond what the work needs.",
|
|
32
33
|
"",
|
|
33
34
|
"## Where the work lives",
|
|
34
35
|
"",
|
package/src/schemas.ts
CHANGED
|
@@ -915,6 +915,16 @@ export const restSkillsSchema = z.object({
|
|
|
915
915
|
/** Where a person browses the address, or `null` without one. */
|
|
916
916
|
page: z.nullable(z.string()),
|
|
917
917
|
status: z.enum(SKILLS_STATUSES),
|
|
918
|
+
/** The pack as its manifest or its README names it, when `ready`: what it is called, what it is for, the language it is written in. */
|
|
919
|
+
pack: z.optional(
|
|
920
|
+
z.object({
|
|
921
|
+
name: z.nullable(z.string()),
|
|
922
|
+
description: z.nullable(z.string()),
|
|
923
|
+
language: z.nullable(z.string()),
|
|
924
|
+
}),
|
|
925
|
+
),
|
|
926
|
+
/** How many skills and documents the deployment indexed at the address, when `ready`. */
|
|
927
|
+
counts: z.optional(z.object({ skills: count, documents: count })),
|
|
918
928
|
/** The skills, when `ready`; empty otherwise. */
|
|
919
929
|
items: z.array(restSkillSchema),
|
|
920
930
|
});
|
|
@@ -1006,6 +1016,106 @@ export const restSkillFileSchema = z.object({
|
|
|
1006
1016
|
});
|
|
1007
1017
|
export type RestSkillFile = z.infer<typeof restSkillFileSchema>;
|
|
1008
1018
|
|
|
1019
|
+
/** An entry of a folder of the pack, as the deployment browses it: a skill, a folder with what it holds, or a file. */
|
|
1020
|
+
export const restSkillBrowseEntrySchema = z.object({
|
|
1021
|
+
kind: z.enum(["skill", "directory", "file"]),
|
|
1022
|
+
/** The path from the repository's root: of the `SKILL.md` for a skill, of the folder, of the file. */
|
|
1023
|
+
path: z.string(),
|
|
1024
|
+
name: z.nullable(z.string()),
|
|
1025
|
+
description: z.nullable(z.string()),
|
|
1026
|
+
/** For a folder: the skills and the documents under it. */
|
|
1027
|
+
skillCount: count,
|
|
1028
|
+
documentCount: count,
|
|
1029
|
+
/** For a file: its size in bytes. */
|
|
1030
|
+
size: z.nullable(count),
|
|
1031
|
+
/** For a folder: the README that introduces it, read with the file route. */
|
|
1032
|
+
overviewPath: z.nullable(z.string()),
|
|
1033
|
+
/** For a skill: its license, and whether the address only describes it. */
|
|
1034
|
+
license: z.nullable(z.string()),
|
|
1035
|
+
describedOnly: z.boolean(),
|
|
1036
|
+
});
|
|
1037
|
+
export type RestSkillBrowseEntry = z.infer<typeof restSkillBrowseEntrySchema>;
|
|
1038
|
+
|
|
1039
|
+
/** `GET .../skills/browse?path=&cursor=&limit=`: what a folder of the pack holds, a page of it (`browse_repo`); `folder` is `null` unless `ready`. */
|
|
1040
|
+
export const restSkillBrowseSchema = z.object({
|
|
1041
|
+
address: z.nullable(z.string()),
|
|
1042
|
+
source: z.string(),
|
|
1043
|
+
page: z.nullable(z.string()),
|
|
1044
|
+
status: z.enum(SKILLS_STATUSES),
|
|
1045
|
+
folder: z.nullable(
|
|
1046
|
+
z.object({
|
|
1047
|
+
path: z.string(),
|
|
1048
|
+
/** The README that introduces the folder, or `null`. */
|
|
1049
|
+
overview: z.nullable(
|
|
1050
|
+
z.object({
|
|
1051
|
+
path: z.string(),
|
|
1052
|
+
title: z.nullable(z.string()),
|
|
1053
|
+
description: z.nullable(z.string()),
|
|
1054
|
+
}),
|
|
1055
|
+
),
|
|
1056
|
+
entries: z.array(restSkillBrowseEntrySchema),
|
|
1057
|
+
/** Where the listing goes on, or `null` at its end. */
|
|
1058
|
+
nextCursor: z.nullable(z.string()),
|
|
1059
|
+
}),
|
|
1060
|
+
),
|
|
1061
|
+
});
|
|
1062
|
+
export type RestSkillBrowse = z.infer<typeof restSkillBrowseSchema>;
|
|
1063
|
+
|
|
1064
|
+
/** What a search of the pack found: a skill, as the listing has it, with the files of it that matched; or a document on its own. */
|
|
1065
|
+
export const restSkillFindItemSchema = z.discriminatedUnion("kind", [
|
|
1066
|
+
z.object({
|
|
1067
|
+
kind: z.literal("skill"),
|
|
1068
|
+
name: z.string(),
|
|
1069
|
+
description: z.string(),
|
|
1070
|
+
directory: z.string(),
|
|
1071
|
+
path: z.string(),
|
|
1072
|
+
page: z.string(),
|
|
1073
|
+
uri: z.nullable(z.string()),
|
|
1074
|
+
translations: z.record(
|
|
1075
|
+
z.string(),
|
|
1076
|
+
z.object({ title: z.nullable(z.string()), description: z.nullable(z.string()) }),
|
|
1077
|
+
),
|
|
1078
|
+
/** The files of the skill that matched, a few, and how many more did. */
|
|
1079
|
+
files: z.array(
|
|
1080
|
+
z.object({
|
|
1081
|
+
path: z.string(),
|
|
1082
|
+
title: z.nullable(z.string()),
|
|
1083
|
+
summary: z.nullable(z.string()),
|
|
1084
|
+
}),
|
|
1085
|
+
),
|
|
1086
|
+
moreFiles: count,
|
|
1087
|
+
license: z.nullable(z.string()),
|
|
1088
|
+
describedOnly: z.boolean(),
|
|
1089
|
+
}),
|
|
1090
|
+
z.object({
|
|
1091
|
+
kind: z.literal("document"),
|
|
1092
|
+
path: z.string(),
|
|
1093
|
+
title: z.nullable(z.string()),
|
|
1094
|
+
summary: z.nullable(z.string()),
|
|
1095
|
+
/** The folder of the skill the document belongs to, or `null` for one on its own. */
|
|
1096
|
+
skillDirectory: z.nullable(z.string()),
|
|
1097
|
+
}),
|
|
1098
|
+
]);
|
|
1099
|
+
export type RestSkillFindItem = z.infer<typeof restSkillFindItemSchema>;
|
|
1100
|
+
|
|
1101
|
+
/** `GET .../skills/find?query=&path=&cursor=&limit=`: what words find in the pack, a page of it in relevance order (`search_repo`); `found` is `null` unless `ready`. */
|
|
1102
|
+
export const restSkillFindSchema = z.object({
|
|
1103
|
+
address: z.nullable(z.string()),
|
|
1104
|
+
source: z.string(),
|
|
1105
|
+
page: z.nullable(z.string()),
|
|
1106
|
+
status: z.enum(SKILLS_STATUSES),
|
|
1107
|
+
found: z.nullable(
|
|
1108
|
+
z.object({
|
|
1109
|
+
query: z.string(),
|
|
1110
|
+
/** The folder the search was kept to, or `null` for the whole pack. */
|
|
1111
|
+
path: z.nullable(z.string()),
|
|
1112
|
+
items: z.array(restSkillFindItemSchema),
|
|
1113
|
+
nextCursor: z.nullable(z.string()),
|
|
1114
|
+
}),
|
|
1115
|
+
),
|
|
1116
|
+
});
|
|
1117
|
+
export type RestSkillFind = z.infer<typeof restSkillFindSchema>;
|
|
1118
|
+
|
|
1009
1119
|
/**
|
|
1010
1120
|
* A document's path (ADR-0009): segments of lowercase letters, digits and hyphens, separated
|
|
1011
1121
|
* by slashes, as a repository keeps files; the document's address, which does not change.
|