@markjaquith/agency 2.37.0 → 2.38.0
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 +5 -11
- package/package.json +1 -1
- package/src/cli.test.ts +71 -37
- package/src/commands/work.test.ts +10 -49
- package/src/commands/work.ts +1 -35
- package/src/utils/chooser.test.ts +47 -0
- package/src/utils/chooser.ts +15 -1
- package/src/utils/interactive.test.tsx +272 -1
- package/src/utils/interactive.tsx +155 -29
- package/src/utils/theme.ts +17 -0
- package/src/workbase/AGENTS.md +3 -4
- package/src/workbase/work-target.test.ts +27 -13
- package/src/workbase/work-target.ts +83 -28
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import type { WorkStatus } from "./schemas"
|
|
3
|
-
import { choose } from "../utils/chooser"
|
|
3
|
+
import { choose, type ChoiceSegment } from "../utils/chooser"
|
|
4
|
+
import { macchiato } from "../utils/theme"
|
|
4
5
|
|
|
5
6
|
export type WorkTarget =
|
|
6
7
|
| {
|
|
@@ -56,58 +57,98 @@ interface PhaseRecord {
|
|
|
56
57
|
export interface WorkTargetChoice {
|
|
57
58
|
readonly label: string
|
|
58
59
|
readonly plainLabel: string
|
|
60
|
+
readonly depth: number
|
|
61
|
+
readonly segments: readonly ChoiceSegment[]
|
|
59
62
|
readonly target: WorkTarget
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const statuses: Record<
|
|
66
|
+
WorkStatus,
|
|
67
|
+
{ readonly icon: string; readonly color: string }
|
|
68
|
+
> = {
|
|
69
|
+
open: { icon: "", color: macchiato.overlay1 },
|
|
70
|
+
working: { icon: "", color: macchiato.blue },
|
|
71
|
+
delegated: { icon: "", color: macchiato.mauve },
|
|
72
|
+
done: { icon: "", color: macchiato.green },
|
|
73
|
+
dropped: { icon: "", color: macchiato.red },
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
const kinds: Record<
|
|
77
|
+
WorkTarget["kind"],
|
|
78
|
+
{ readonly icon: string; readonly color: string }
|
|
79
|
+
> = {
|
|
80
|
+
epic: { icon: "", color: macchiato.mauve },
|
|
81
|
+
task: { icon: "", color: macchiato.sapphire },
|
|
82
|
+
phase: { icon: "", color: macchiato.yellow },
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hexToAnsi = (color: string) => {
|
|
86
|
+
const value = color.slice(1)
|
|
87
|
+
return [0, 2, 4]
|
|
88
|
+
.map((offset) => Number.parseInt(value.slice(offset, offset + 2), 16))
|
|
89
|
+
.join(";")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const colorize = (item: { readonly icon: string; readonly color: string }) =>
|
|
93
|
+
`\x1b[38;2;${hexToAnsi(item.color)}m${item.icon}\x1b[0m`
|
|
94
|
+
|
|
70
95
|
const label = (
|
|
71
|
-
indent: string,
|
|
72
96
|
kind: WorkTarget["kind"],
|
|
73
97
|
id: string,
|
|
74
98
|
description?: string,
|
|
75
99
|
status?: WorkStatus,
|
|
76
100
|
) =>
|
|
77
|
-
`${
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
`${status === undefined ? "" : `${colorize(statuses[status])} `}${colorize(kinds[kind])} ${id}${description === undefined ? "" : `\x1b[2m - ${description}\x1b[0m`}`
|
|
102
|
+
|
|
103
|
+
const segments = (
|
|
104
|
+
kind: WorkTarget["kind"],
|
|
105
|
+
id: string,
|
|
106
|
+
description?: string,
|
|
107
|
+
status?: WorkStatus,
|
|
108
|
+
): readonly ChoiceSegment[] => [
|
|
109
|
+
...(status === undefined
|
|
110
|
+
? []
|
|
111
|
+
: [
|
|
112
|
+
{ text: statuses[status].icon, color: statuses[status].color },
|
|
113
|
+
{ text: " " },
|
|
114
|
+
]),
|
|
115
|
+
{ text: kinds[kind].icon, color: kinds[kind].color },
|
|
116
|
+
{ text: ` ${id}` },
|
|
117
|
+
...(description === undefined
|
|
118
|
+
? []
|
|
119
|
+
: [{ text: ` - ${description}`, color: macchiato.overlay0 }]),
|
|
120
|
+
]
|
|
84
121
|
|
|
85
122
|
const plainLabel = (
|
|
86
|
-
indent: string,
|
|
87
123
|
kind: WorkTarget["kind"],
|
|
88
124
|
id: string,
|
|
89
125
|
description?: string,
|
|
90
126
|
status?: WorkStatus,
|
|
91
127
|
) =>
|
|
92
|
-
`${
|
|
128
|
+
`${status === undefined ? "" : `[${status}] `}${kind} ${id}${description === undefined ? "" : ` - ${description}`}`
|
|
93
129
|
|
|
94
130
|
const taskChoices = (
|
|
95
131
|
task: TaskRecord,
|
|
96
132
|
phaseRecords: readonly PhaseRecord[],
|
|
97
|
-
|
|
133
|
+
depth: number,
|
|
98
134
|
): readonly WorkTargetChoice[] => {
|
|
99
135
|
const multiPhase = "phases" in task.data
|
|
100
136
|
const choices: WorkTargetChoice[] = [
|
|
101
137
|
{
|
|
102
138
|
label: label(
|
|
103
|
-
indent,
|
|
104
139
|
"task",
|
|
105
140
|
task.id,
|
|
106
141
|
task.data.description,
|
|
107
142
|
multiPhase ? undefined : (task.data.status ?? "open"),
|
|
108
143
|
),
|
|
109
144
|
plainLabel: plainLabel(
|
|
110
|
-
|
|
145
|
+
"task",
|
|
146
|
+
task.id,
|
|
147
|
+
task.data.description,
|
|
148
|
+
multiPhase ? undefined : (task.data.status ?? "open"),
|
|
149
|
+
),
|
|
150
|
+
depth,
|
|
151
|
+
segments: segments(
|
|
111
152
|
"task",
|
|
112
153
|
task.id,
|
|
113
154
|
task.data.description,
|
|
@@ -132,14 +173,19 @@ const taskChoices = (
|
|
|
132
173
|
renderedPhases.add(record.id)
|
|
133
174
|
choices.push({
|
|
134
175
|
label: label(
|
|
135
|
-
`${indent} `,
|
|
136
176
|
"phase",
|
|
137
177
|
record.id,
|
|
138
178
|
record.data.description,
|
|
139
179
|
record.data.status ?? "open",
|
|
140
180
|
),
|
|
141
181
|
plainLabel: plainLabel(
|
|
142
|
-
|
|
182
|
+
"phase",
|
|
183
|
+
record.id,
|
|
184
|
+
record.data.description,
|
|
185
|
+
record.data.status ?? "open",
|
|
186
|
+
),
|
|
187
|
+
depth: depth + 1,
|
|
188
|
+
segments: segments(
|
|
143
189
|
"phase",
|
|
144
190
|
record.id,
|
|
145
191
|
record.data.description,
|
|
@@ -158,14 +204,19 @@ const taskChoices = (
|
|
|
158
204
|
if (renderedPhases.has(record.id)) continue
|
|
159
205
|
choices.push({
|
|
160
206
|
label: label(
|
|
161
|
-
`${indent} `,
|
|
162
207
|
"phase",
|
|
163
208
|
record.id,
|
|
164
209
|
record.data.description,
|
|
165
210
|
record.data.status ?? "open",
|
|
166
211
|
),
|
|
167
212
|
plainLabel: plainLabel(
|
|
168
|
-
|
|
213
|
+
"phase",
|
|
214
|
+
record.id,
|
|
215
|
+
record.data.description,
|
|
216
|
+
record.data.status ?? "open",
|
|
217
|
+
),
|
|
218
|
+
depth: depth + 1,
|
|
219
|
+
segments: segments(
|
|
169
220
|
"phase",
|
|
170
221
|
record.id,
|
|
171
222
|
record.data.description,
|
|
@@ -200,21 +251,23 @@ export const buildWorkTargetChoices = (
|
|
|
200
251
|
|
|
201
252
|
for (const epic of epicRecords) {
|
|
202
253
|
choices.push({
|
|
203
|
-
label: label("
|
|
204
|
-
plainLabel: plainLabel("
|
|
254
|
+
label: label("epic", epic.id, epic.data.description),
|
|
255
|
+
plainLabel: plainLabel("epic", epic.id, epic.data.description),
|
|
256
|
+
depth: 0,
|
|
257
|
+
segments: segments("epic", epic.id, epic.data.description),
|
|
205
258
|
target: { kind: "epic", epicId: epic.id, path: epic.path },
|
|
206
259
|
})
|
|
207
260
|
for (const child of epic.data.tasks) {
|
|
208
261
|
const task = tasks.get(child.id)
|
|
209
262
|
if (!task || nestedTasks.has(task.id)) continue
|
|
210
263
|
nestedTasks.add(task.id)
|
|
211
|
-
choices.push(...taskChoices(task, phases.get(task.id) ?? [],
|
|
264
|
+
choices.push(...taskChoices(task, phases.get(task.id) ?? [], 1))
|
|
212
265
|
}
|
|
213
266
|
}
|
|
214
267
|
|
|
215
268
|
for (const task of taskRecords) {
|
|
216
269
|
if (nestedTasks.has(task.id)) continue
|
|
217
|
-
choices.push(...taskChoices(task, phases.get(task.id) ?? [],
|
|
270
|
+
choices.push(...taskChoices(task, phases.get(task.id) ?? [], 0))
|
|
218
271
|
}
|
|
219
272
|
|
|
220
273
|
return choices
|
|
@@ -232,6 +285,8 @@ export const pickWorkTarget: PickWorkTarget = (choices, command) =>
|
|
|
232
285
|
key: String(index),
|
|
233
286
|
label: choice.label,
|
|
234
287
|
plainLabel: choice.plainLabel,
|
|
288
|
+
depth: choice.depth,
|
|
289
|
+
segments: choice.segments,
|
|
235
290
|
value: choice.target,
|
|
236
291
|
})),
|
|
237
292
|
command,
|