@markjaquith/agency 2.37.1 → 2.38.1

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.
@@ -61,14 +61,14 @@ describe("work target choices", () => {
61
61
  )
62
62
 
63
63
  expect(choices.map((choice) => choice.label)).toEqual([
64
- "\x1b[35m\x1b[0m delivery\x1b[2m - Ship the release\x1b[0m",
65
- " \x1b[36m󰗡\x1b[0m multi",
66
- " \x1b[34m◐\x1b[0m \x1b[33m󰔚\x1b[0m build",
67
- " \x1b[32m✓\x1b[0m \x1b[33m󰔚\x1b[0m verify",
68
- " \x1b[31m⊘\x1b[0m \x1b[33m󰔚\x1b[0m unlisted",
69
- " \x1b[32m✓\x1b[0m \x1b[36m󰗡\x1b[0m single",
70
- "\x1b[2m○\x1b[0m \x1b[36m󰗡\x1b[0m standalone\x1b[2m - Independent work\x1b[0m",
71
- "\x1b[35m↗\x1b[0m \x1b[36m󰗡\x1b[0m delegated",
64
+ "\x1b[38;2;198;160;246m\x1b[0m delivery\x1b[2m - Ship the release\x1b[0m",
65
+ "\x1b[38;2;125;196;228m󰗡\x1b[0m multi",
66
+ "\x1b[38;2;138;173;244m󰔟\x1b[0m \x1b[38;2;238;212;159m󰔚\x1b[0m build",
67
+ "\x1b[38;2;166;218;149m󰄬\x1b[0m \x1b[38;2;238;212;159m󰔚\x1b[0m verify",
68
+ "\x1b[38;2;237;135;150m󰅖\x1b[0m \x1b[38;2;238;212;159m󰔚\x1b[0m unlisted",
69
+ "\x1b[38;2;166;218;149m󰄬\x1b[0m \x1b[38;2;125;196;228m󰗡\x1b[0m single",
70
+ "\x1b[38;2;128;135;162m󰄱\x1b[0m \x1b[38;2;125;196;228m󰗡\x1b[0m standalone\x1b[2m - Independent work\x1b[0m",
71
+ "\x1b[38;2;198;160;246m󰁕\x1b[0m \x1b[38;2;125;196;228m󰗡\x1b[0m delegated",
72
72
  ])
73
73
  expect(choices.map((choice) => choice.target.kind)).toEqual([
74
74
  "epic",
@@ -82,13 +82,27 @@ describe("work target choices", () => {
82
82
  ])
83
83
  expect(choices.map((choice) => choice.plainLabel)).toEqual([
84
84
  "epic delivery - Ship the release",
85
- " task multi",
86
- " [working] phase build",
87
- " [done] phase verify",
88
- " [dropped] phase unlisted",
89
- " [done] task single",
85
+ "task multi",
86
+ "[working] phase build",
87
+ "[done] phase verify",
88
+ "[dropped] phase unlisted",
89
+ "[done] task single",
90
90
  "[open] task standalone - Independent work",
91
91
  "[delegated] task delegated",
92
92
  ])
93
+ expect(choices.map((choice) => choice.depth)).toEqual([
94
+ 0, 1, 2, 2, 2, 1, 0, 0,
95
+ ])
96
+ expect(choices[0]!.segments).toEqual([
97
+ { text: "", color: "#c6a0f6" },
98
+ { text: " delivery" },
99
+ { text: " - Ship the release", color: "#6e738d" },
100
+ ])
101
+ expect(choices[2]!.segments).toEqual([
102
+ { text: "󰔟", color: "#8aadf4" },
103
+ { text: " " },
104
+ { text: "󰔚", color: "#eed49f" },
105
+ { text: " build" },
106
+ ])
93
107
  })
94
108
  })
@@ -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 statusIcons: Record<WorkStatus, string> = {
63
- open: "\x1b[2m○\x1b[0m",
64
- working: "\x1b[34m◐\x1b[0m",
65
- delegated: "\x1b[35m↗\x1b[0m",
66
- done: "\x1b[32m✓\x1b[0m",
67
- dropped: "\x1b[31m⊘\x1b[0m",
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
- `${indent}${status === undefined ? "" : `${statusIcons[status]} `}${
78
- {
79
- epic: "\x1b[35m\x1b[0m",
80
- task: "\x1b[36m󰗡\x1b[0m",
81
- phase: "\x1b[33m󰔚\x1b[0m",
82
- }[kind]
83
- } ${id}${description === undefined ? "" : `\x1b[2m - ${description}\x1b[0m`}`
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
- `${indent}${status === undefined ? "" : `[${status}] `}${kind} ${id}${description === undefined ? "" : ` - ${description}`}`
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
- indent: string,
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
- indent,
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
- `${indent} `,
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
- `${indent} `,
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("", "epic", epic.id, epic.data.description),
204
- plainLabel: plainLabel("", "epic", epic.id, epic.data.description),
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,