@skillcdn/console 0.1.20 → 0.1.21
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/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +6 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +4 -6
- package/dist/cli/help.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 +2 -1
- package/dist/rules.js.map +1 -1
- package/docs/specs/cli.md +2 -2
- package/docs/specs/rest.md +1 -2
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +2 -2
- package/src/cli/cli.ts +6 -0
- package/src/cli/help.ts +4 -6
- 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 +2 -1
- package/src/styles/console.css +59 -17
package/docs/specs/cli.md
CHANGED
|
@@ -24,7 +24,7 @@ The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-
|
|
|
24
24
|
| `login --url <origin> [--agent <name>] [--token-stdin] [--default]` | `POST /api/v1/connect`, then `POST /api/v1/connect/claim` until a person approved, then `GET /api/v1/me` with the token; `DELETE /api/v1/me/token` with the token a repeated login replaces | the address and the code for a person; then who the token is, and whether this is the default console; keeps the login |
|
|
25
25
|
| `logins` | nothing | one line per login: the console, the account, what the token is called, since when, and which is the default; never a token |
|
|
26
26
|
| `logout [--url <origin>] [--account <login>]` | `DELETE /api/v1/me/token` | that the login's token is given up and forgotten, or that the console was out of reach and the token is left to the Agents page |
|
|
27
|
-
| `whoami` | `GET /api/v1/me` | the login, the role, the workspace, the address, and the project this directory works in; then the command's version and where it is installed, the console's version and the range it takes, and the update to make when they differ |
|
|
27
|
+
| `whoami` | `GET /api/v1/me` | the login, the role, the workspace, the address, and the project this directory works in; with a project, where the organization's rules and the skill working-the-board are read; then the command's version and where it is installed, the console's version and the range it takes, and the update to make when they differ |
|
|
28
28
|
| `projects` | `GET /api/v1/projects` | one line per project: key, name, what the person is in it, what waits in it |
|
|
29
29
|
| `use <key> [--url <origin>] [--account <login>]` | `GET /api/v1/projects/<key>` | that the directory works in the project from now on, at which console and as which account where it binds them, and that `console rules` says what its people ask; writes `.skillcdn-console.json` |
|
|
30
30
|
| `work [--person <login>]` | `GET /api/v1/work`, `GET /api/v1/people/<id>/work` | what is on the person's plate across their projects: the open tasks assigned to them, the sooner due first, each with its project; and the decisions waiting. `--person` shows another person's plate, for an administrator or an owner of a project they are in: their open tasks within the projects the asker may see, without the decisions. Needs no project |
|
|
@@ -32,7 +32,7 @@ The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-
|
|
|
32
32
|
| `task <number\|id>` | `GET .../tasks/<ref>`, the decisions about it, the runs on it, `GET .../tasks/<id>/comments` | the task in full, with its plan and its moments, and the discussion on it: each comment whole, with its files and the command that reads each |
|
|
33
33
|
| `task new <title> [--body <markdown>\|--file <path>] [--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--assignee <login\|me>] [--parent <number\|id>]` | `POST .../tasks` | the number and the id |
|
|
34
34
|
| `task edit <number\|id> [--title <title>] [--body ...] [the same flags; `none` clears a field]` | `GET .../tasks/<ref>`, then `PATCH .../tasks/<id>` | the task in full, as it is now |
|
|
35
|
-
| `task archive <number\|id>` | `GET .../tasks/<ref>`, then `POST .../tasks/<id>/archive` | the task's line, put away ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md)): out of the listings, the board and the calendar, open by its number still
|
|
35
|
+
| `task archive <number\|id>` | `GET .../tasks/<ref>`, then `POST .../tasks/<id>/archive` | the task's line, put away ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md)), whatever its state ([ADR-0030](../adr/0030-a-task-is-put-away-from-any-state.md)): out of the listings, the board and the calendar, open by its number still |
|
|
36
36
|
| `task archive --goal <id>` | `POST .../tasks/archive` | how many done or dropped tasks of the goal were put away; for an owner |
|
|
37
37
|
| `task restore <number\|id>` | `GET .../tasks/<ref>`, then `POST .../tasks/<id>/restore` | the task's line, back on the board |
|
|
38
38
|
| `goals [--category <key>] [--state <state>]` | `GET .../goals?category=&state=` | one line per goal of the roadmap: id, state, title, category, the days it starts and is meant to be reached by, how far it is ([ADR-0026](../adr/0026-a-goal-is-planned-in-days-and-sorted-into-a-category-and-the-roadmap-is-read-as-months.md)) |
|
package/docs/specs/rest.md
CHANGED
|
@@ -59,7 +59,6 @@ The API is the board as its members see it: the people, the projects, and in eac
|
|
|
59
59
|
| 409 | `decision.answered` | The decision has an answer already. |
|
|
60
60
|
| 409 | `document.conflict` | The page has moved on since the version the write started from (`baseVersion`), or was written at that path by someone else first. |
|
|
61
61
|
| 409 | `document.archived` | The page is archived: restore it before writing to it or attaching to it. |
|
|
62
|
-
| 409 | `task.open` | The task is open: only one done or dropped is put away ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md)). |
|
|
63
62
|
| 409 | `document.too_many_versions`, `document.too_many_files` | The page carries as many versions, or files, as one may (`MAX_VERSIONS_PER_DOCUMENT`, `MAX_FILES_PER_DOCUMENT`). |
|
|
64
63
|
| 409 | `token.too_many` | The person holds as many live tokens as one may (`MAX_TOKENS_PER_PERSON`). |
|
|
65
64
|
| 409 | `connect.approved` | The connection was approved already; the command claims its token. |
|
|
@@ -188,7 +187,7 @@ One task, by its id or by its number in the project (`/tasks/7`): the pages hold
|
|
|
188
187
|
|
|
189
188
|
### `POST .../tasks/<id>/archive`, `POST .../tasks/<id>/restore` and `POST /api/v1/projects/<key>/tasks/archive`
|
|
190
189
|
|
|
191
|
-
Putting a task away, and bringing it back ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md)). A task
|
|
190
|
+
Putting a task away, and bringing it back ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md), [ADR-0030](../adr/0030-a-task-is-put-away-from-any-state.md)). A task is put away, whatever its state, by whoever may work in the project, with a session or a token, and brought back the same way: putting away is a moment the board keeps (`archivedAt`), not a state, so the task keeps its state, counts toward its goal as before, and keeps its runs, decisions, comments and events; it is out of the listings, the board and the calendar, and open at its own address by id or by number still. Each answers the task and writes `task.archived` or `task.restored` with the task's `number` and `title`; what is so already answers the task and writes nothing. `POST .../tasks/archive`, for an owner, takes `{ "goalId" }` and puts every done or dropped task of that goal away at once, one event each, answering `{ "archived": n }`, how many went; a goal with nothing to put away, or that is not the project's, is `0`. The console puts away by itself what was done or dropped and not touched for `TASK_ARCHIVE_AFTER_DAYS` ([deploy/README.md](../../deploy/README.md#environment-contract)), each with a `task.archived` event that names no actor.
|
|
192
191
|
|
|
193
192
|
### `GET .../tasks/<id or number>/comments` and `POST .../tasks/<id or number>/comments`
|
|
194
193
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillcdn/console",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "What a custom SkillCDN console is built from: the API client and schemas, the components, the composition of the default console, and the console command agents work the board with.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/skillcdn/console#readme",
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: Needs a shell with the console command on the PATH (npm install -g @skillcdn/console, Node.js 24 or newer), signed in by a person. Nothing else; the command needs no key of its own and the agent never sees a token.
|
|
6
6
|
metadata:
|
|
7
7
|
author: skillcdn
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.14"
|
|
9
9
|
skillcdn:
|
|
10
10
|
translations:
|
|
11
11
|
ko:
|
|
@@ -50,7 +50,7 @@ Each phase ends with something on the board. Only phase 4 waits for a person.
|
|
|
50
50
|
|
|
51
51
|
### Phase 1: Read
|
|
52
52
|
|
|
53
|
-
Told which task, go to phase 2: `console take` prints the rules of the board with the task, and they are read there, once. Choosing one, or checking one before taking it, `console rules` says which tasks to take and what to load, and `console task <number>` shows a task in full, with its plan, the decisions about it, the runs on it and the discussion on it, without taking it. A run open on it means another agent was at it, and may still be: the console cannot tell ([ADR-0020](/docs/adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), so read its reports and when it was last heard from, and take the task only when the person said to, since taking it ends that run as taken over; otherwise say so and stop. A task that is past its due day is still the task; the first report says so, and what the day was for is kept in mind. A task put away
|
|
53
|
+
Told which task, go to phase 2: `console take` prints the rules of the board with the task, and they are read there, once. Choosing one, or checking one before taking it, `console rules` says which tasks to take and what to load, and `console task <number>` shows a task in full, with its plan, the decisions about it, the runs on it and the discussion on it, without taking it. A run open on it means another agent was at it, and may still be: the console cannot tell ([ADR-0020](/docs/adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), so read its reports and when it was last heard from, and take the task only when the person said to, since taking it ends that run as taken over; otherwise say so and stop. A task that is past its due day is still the task; the first report says so, and what the day was for is kept in mind. A task put away ([ADR-0029](/docs/adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md), from any state since [ADR-0030](/docs/adr/0030-a-task-is-put-away-from-any-state.md)) is out of `console tasks` unless `--archived` asks for the archive, and `console task <number>` opens it still. Read what the body links to, with `console doc <path>` for the project's pages and `console skills` for the organization's skills, before anything is made.
|
|
54
54
|
|
|
55
55
|
### Phase 2: Take
|
|
56
56
|
|
package/src/cli/cli.ts
CHANGED
|
@@ -844,6 +844,12 @@ const whoami: Command = async (args, io) => {
|
|
|
844
844
|
() =>
|
|
845
845
|
[
|
|
846
846
|
`${person.login} (${person.role}) at ${me.workspace.name}, ${session.url}${key === undefined ? "; no project named here" : `; project ${key}`}`,
|
|
847
|
+
// The pack's rules send an agent here first; from here the rules and the skill are the next steps.
|
|
848
|
+
...(key === undefined
|
|
849
|
+
? []
|
|
850
|
+
: [
|
|
851
|
+
"What this organization asks of agents: console rules. How the board is worked: the skill working-the-board, shipped with this command (console skills says where it is).",
|
|
852
|
+
]),
|
|
847
853
|
`This command is ${io.version ?? "of a version it does not know"}${io.install === undefined ? "" : `, ${describeInstall(io.install)}`}; ${consoleWords}.`,
|
|
848
854
|
...(verdict === "behind"
|
|
849
855
|
? [`A newer command exists. Update it: ${updateWords(io, me.console?.version)}`]
|
package/src/cli/help.ts
CHANGED
|
@@ -131,17 +131,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
131
131
|
task: {
|
|
132
132
|
usage: `task <number|id> | task new <title> [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task edit <number|id> [--title <title>] [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task archive <number|id> | task archive --goal <id> | task restore <number|id>`,
|
|
133
133
|
about:
|
|
134
|
-
"One task in full, with its plan (category, goal, start and due days), what happened to it (when it was started and done, which the board keeps itself, and when it was put away), the decisions about it, the runs on it and the discussion on it, each comment whole with its files; or a new task written down; or a task changed, only in what the flags say, where none clears a field (priorities: low, normal, high, urgent). archive puts a
|
|
134
|
+
"One task in full, with its plan (category, goal, start and due days), what happened to it (when it was started and done, which the board keeps itself, and when it was put away), the decisions about it, the runs on it and the discussion on it, each comment whole with its files; or a new task written down; or a task changed, only in what the flags say, where none clears a field (priorities: low, normal, high, urgent). archive puts a task away, whatever its state (ADR-0029, ADR-0030): out of the listings, the board and the calendar, open by its number still; with --goal, every done or dropped task of the goal at once, for an owner. restore brings one back.",
|
|
135
135
|
limits: [
|
|
136
136
|
`title: one line, up to ${MAX_TITLE_LENGTH} characters, saying what is to be done on its own; what it refers to goes in the body`,
|
|
137
137
|
`body: ${MARKDOWN}`,
|
|
138
138
|
...TASK_FIELD_LIMITS,
|
|
139
139
|
],
|
|
140
|
-
refusals: [
|
|
141
|
-
"task.not_found: no task has that number or id",
|
|
142
|
-
"task.open: an open task is not put away; one done or dropped is",
|
|
143
|
-
...TASK_FIELD_REFUSALS,
|
|
144
|
-
],
|
|
140
|
+
refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
|
|
145
141
|
},
|
|
146
142
|
goals: {
|
|
147
143
|
usage: "goals [--category <key>] [--state <state>]",
|
|
@@ -450,6 +446,8 @@ const preamble = (version: string | undefined): string =>
|
|
|
450
446
|
`console${version === undefined ? "" : ` ${version}`}: the board of your organization's work with AI agents, from the command line.
|
|
451
447
|
You act as the person whose token you hold; everything you send is shown to people as text.
|
|
452
448
|
The rules of this organization come with the task: console take prints them; console rules reads them any time.
|
|
449
|
+
How the board is worked is the skill working-the-board, installed with this command: console skills prints
|
|
450
|
+
where it is; read it before the first command of a run.
|
|
453
451
|
|
|
454
452
|
Usage: console <command> [arguments] [--json] [--url <origin>] [--project <key>]
|
|
455
453
|
`;
|
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,8 +23,9 @@ 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
|
"",
|
package/src/styles/console.css
CHANGED
|
@@ -718,6 +718,34 @@ table {
|
|
|
718
718
|
gap: var(--sc-space-2);
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
+
/* A page's name with a word about it beside: on one line where the window is wide, so that the
|
|
722
|
+
head stays low over the board; under the name where it is not. */
|
|
723
|
+
.sc-page-heading {
|
|
724
|
+
display: flex;
|
|
725
|
+
flex: 1 1 auto;
|
|
726
|
+
flex-wrap: wrap;
|
|
727
|
+
align-items: baseline;
|
|
728
|
+
min-width: 0;
|
|
729
|
+
column-gap: var(--sc-space-4);
|
|
730
|
+
row-gap: var(--sc-space-1);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.sc-page-lead {
|
|
734
|
+
flex: 1 1 16rem;
|
|
735
|
+
min-width: 0;
|
|
736
|
+
margin: 0;
|
|
737
|
+
color: var(--sc-color-text-muted);
|
|
738
|
+
font-size: var(--sc-text-sm);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
@media (min-width: 60rem) {
|
|
742
|
+
.sc-page-lead {
|
|
743
|
+
overflow: hidden;
|
|
744
|
+
text-overflow: ellipsis;
|
|
745
|
+
white-space: nowrap;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
721
749
|
.sc-lead {
|
|
722
750
|
max-width: var(--sc-width-prose);
|
|
723
751
|
margin-bottom: var(--sc-space-5);
|
|
@@ -786,6 +814,12 @@ table {
|
|
|
786
814
|
pointer-events: none;
|
|
787
815
|
}
|
|
788
816
|
|
|
817
|
+
/* A column's frame stands in front of what scrolls within it: a card scrolling out goes under
|
|
818
|
+
the lit edge, never over it. The other panes do not scroll, and keep the rim under the content. */
|
|
819
|
+
.sc-column::before {
|
|
820
|
+
z-index: 1;
|
|
821
|
+
}
|
|
822
|
+
|
|
789
823
|
.sc-panel {
|
|
790
824
|
margin-bottom: var(--sc-space-5);
|
|
791
825
|
padding: var(--sc-space-5);
|
|
@@ -3768,16 +3802,16 @@ table {
|
|
|
3768
3802
|
}
|
|
3769
3803
|
}
|
|
3770
3804
|
|
|
3771
|
-
/* A card is dragged to another column from anywhere on it but its
|
|
3772
|
-
mouse at once, under a finger once it has rested a moment, so that a finger that moves
|
|
3773
|
-
The
|
|
3774
|
-
|
|
3805
|
+
/* A card is dragged to another column from anywhere on it but its menu, its title included: with
|
|
3806
|
+
a mouse at once, under a finger once it has rested a moment, so that a finger that moves
|
|
3807
|
+
scrolls. The column under the dragged card says it would take it, and the card itself, whole,
|
|
3808
|
+
follows the pointer while a faded one keeps its place. */
|
|
3775
3809
|
.sc-card {
|
|
3776
3810
|
position: relative;
|
|
3777
3811
|
}
|
|
3778
3812
|
|
|
3779
3813
|
.sc-card-movable {
|
|
3780
|
-
cursor:
|
|
3814
|
+
cursor: grab;
|
|
3781
3815
|
}
|
|
3782
3816
|
|
|
3783
3817
|
/* A finger resting on a card is neither selecting its words nor asking for the page's menu. */
|
|
@@ -3789,13 +3823,13 @@ table {
|
|
|
3789
3823
|
}
|
|
3790
3824
|
|
|
3791
3825
|
.sc-card-dragging {
|
|
3792
|
-
opacity: 0.
|
|
3826
|
+
opacity: 0.4;
|
|
3793
3827
|
}
|
|
3794
3828
|
|
|
3795
3829
|
.sc-board-dragging,
|
|
3796
3830
|
.sc-board-dragging .sc-card,
|
|
3797
3831
|
.sc-board-dragging .sc-card * {
|
|
3798
|
-
cursor:
|
|
3832
|
+
cursor: grabbing;
|
|
3799
3833
|
user-select: none;
|
|
3800
3834
|
}
|
|
3801
3835
|
|
|
@@ -3828,6 +3862,9 @@ table {
|
|
|
3828
3862
|
height: 1rem;
|
|
3829
3863
|
}
|
|
3830
3864
|
|
|
3865
|
+
/* Shown on the card under the pointer or with the focus, while the menu is open, and on the card
|
|
3866
|
+
in hand, which is the card as it was hovered: its menu's dots, and the room they take. */
|
|
3867
|
+
.sc-drag-ghost .sc-card-menu-button,
|
|
3831
3868
|
.sc-card:hover .sc-card-menu-button,
|
|
3832
3869
|
.sc-card:focus-within .sc-card-menu-button,
|
|
3833
3870
|
.sc-card-menu-button[aria-expanded="true"] {
|
|
@@ -3860,12 +3897,18 @@ table {
|
|
|
3860
3897
|
z-index: 60;
|
|
3861
3898
|
display: flex;
|
|
3862
3899
|
flex-direction: column;
|
|
3863
|
-
min-width:
|
|
3900
|
+
min-width: 8rem;
|
|
3864
3901
|
padding: var(--sc-space-1);
|
|
3865
3902
|
border: var(--sc-border);
|
|
3866
|
-
border-radius: var(--sc-radius-
|
|
3903
|
+
border-radius: var(--sc-radius-md);
|
|
3867
3904
|
background: var(--sc-color-bg-inset);
|
|
3868
3905
|
box-shadow: var(--sc-shadow-stage);
|
|
3906
|
+
font-size: var(--sc-text-sm);
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
.sc-card-menu .sc-menu-link {
|
|
3910
|
+
padding: calc(var(--sc-space-1) + 1px) var(--sc-space-2);
|
|
3911
|
+
border-radius: var(--sc-radius-sm);
|
|
3869
3912
|
}
|
|
3870
3913
|
|
|
3871
3914
|
.sc-column-over {
|
|
@@ -3878,17 +3921,16 @@ table {
|
|
|
3878
3921
|
top: 0;
|
|
3879
3922
|
left: 0;
|
|
3880
3923
|
z-index: 50;
|
|
3881
|
-
max-width: 18rem;
|
|
3882
|
-
padding: var(--sc-space-2) var(--sc-space-3);
|
|
3883
|
-
border: 1px solid var(--sc-color-accent);
|
|
3884
|
-
border-radius: var(--sc-radius-lg);
|
|
3885
|
-
background: var(--sc-color-bg-raised);
|
|
3886
|
-
box-shadow: var(--sc-glow);
|
|
3887
|
-
color: var(--sc-color-text);
|
|
3888
|
-
font-size: var(--sc-text-sm);
|
|
3889
3924
|
pointer-events: none;
|
|
3890
3925
|
}
|
|
3891
3926
|
|
|
3927
|
+
.sc-drag-ghost .sc-card {
|
|
3928
|
+
border-color: var(--sc-color-accent);
|
|
3929
|
+
background: var(--sc-color-bg-raised);
|
|
3930
|
+
box-shadow: var(--sc-shadow-stage);
|
|
3931
|
+
transform: scale(1.02);
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3892
3934
|
/* A goal's page reads as the board does: the facts in a row at its head, then a switch between
|
|
3893
3935
|
the views of its tasks, then the view, which takes the rest of the height as the board does. */
|
|
3894
3936
|
.sc-goal-page {
|