jackherd 1.11.2 → 1.12.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +69 -51
  3. package/dist/{chunk-RMMXD4G5.js → chunk-V4HOLGFP.js} +2 -2
  4. package/dist/config.d.ts +10 -0
  5. package/dist/config.js +15 -5
  6. package/dist/plan-format.d.ts +11 -6
  7. package/dist/plan-format.js +8 -2
  8. package/docs/follow-dark.svg +2 -2
  9. package/docs/follow.svg +2 -2
  10. package/docs/workflow-dark.svg +56 -64
  11. package/docs/workflow.svg +56 -64
  12. package/herdr-plugin/herdr-plugin.toml +1 -1
  13. package/install.sh +1 -1
  14. package/kit/workflow/skills/implement/SKILL.md +1 -1
  15. package/kit/workflow/skills/orchestrate/SKILL.md +1 -1
  16. package/kit/workflow/skills/orchestrate/references/draft-review.md +4 -0
  17. package/kit/workflow/skills/orchestrate/references/events.md +8 -1
  18. package/kit/workflow/skills/plan/SKILL.md +2 -0
  19. package/package.json +2 -2
  20. package/schemas/jackherd.schema.json +18 -1
  21. package/src/config/link-kinds.ts +1 -1
  22. package/src/config/schema.ts +5 -2
  23. package/src/config/tones.ts +9 -2
  24. package/src/context/fill.ts +4 -10
  25. package/src/context/session-log.ts +108 -2
  26. package/src/plan-format/lanes.ts +10 -1
  27. package/src/plan-format/phases.ts +11 -3
  28. package/src/plan-format/progress.ts +4 -2
  29. package/src/plan-format/waiting.ts +12 -0
  30. package/src/plans/active-tasks.ts +31 -8
  31. package/src/plans/appointment-rows.ts +42 -53
  32. package/src/plans/board-rows.ts +14 -0
  33. package/src/plans/calendar-days.ts +29 -0
  34. package/src/plans/click-hint.ts +94 -0
  35. package/src/plans/command.ts +8 -9
  36. package/src/plans/current-phase.ts +22 -10
  37. package/src/plans/current-task.ts +18 -3
  38. package/src/plans/frame.ts +18 -26
  39. package/src/plans/git.ts +90 -31
  40. package/src/plans/groomed.ts +13 -7
  41. package/src/plans/lane-reason.ts +61 -0
  42. package/src/plans/lane-summary.ts +75 -24
  43. package/src/plans/layout-phase.ts +123 -76
  44. package/src/plans/layout-summary.ts +133 -83
  45. package/src/plans/layout.ts +451 -345
  46. package/src/plans/legend.ts +29 -0
  47. package/src/plans/link-chips.ts +26 -8
  48. package/src/plans/live.ts +238 -84
  49. package/src/plans/next-rows.ts +5 -37
  50. package/src/plans/phase-source.ts +8 -3
  51. package/src/plans/plan-chips.ts +6 -3
  52. package/src/plans/plan-heads.ts +1 -1
  53. package/src/plans/planned.ts +4 -1
  54. package/src/plans/process-slots.ts +36 -0
  55. package/src/plans/pull-requests.ts +9 -1
  56. package/src/plans/read-cache.ts +18 -3
  57. package/src/plans/refresh-pace.ts +13 -0
  58. package/src/plans/row-focus.ts +86 -15
  59. package/src/plans/selection.ts +124 -89
  60. package/src/plans/sent-files.ts +1 -1
  61. package/src/plans/settings.ts +5 -2
  62. package/src/plans/shimmer.ts +124 -0
  63. package/src/plans/signals-read.ts +38 -10
  64. package/src/plans/slice-gate.ts +51 -0
  65. package/src/plans/snapshot.ts +185 -84
  66. package/src/plans/source-asked.ts +1 -1
  67. package/src/plans/source-rows.ts +60 -18
  68. package/src/plans/sprint-html.ts +1 -1
  69. package/src/plans/status-pace.ts +45 -0
  70. package/src/plans/subagent-tasks.ts +1 -1
  71. package/src/plans/subagents-read.ts +61 -24
  72. package/src/plans/subagents.ts +2 -2
  73. package/src/plans/symbols.ts +4 -1
  74. package/src/plans/task-brief.ts +143 -0
  75. package/src/plans/texts.ts +181 -39
  76. package/src/plans/view.ts +198 -32
  77. package/src/plans/waiting-click.ts +54 -110
  78. package/src/plans/watch-burst.ts +25 -0
  79. package/src/plans/worktree-stamp.ts +35 -0
  80. package/src/plans/your-turn-rows.ts +102 -0
  81. package/src/plans/your-turn.ts +226 -0
  82. package/src/terminal/herdr.ts +25 -5
  83. package/src/terminal/restore.ts +44 -0
  84. package/src/terminal/text.ts +2 -2
  85. package/src/terminal/width.ts +9 -0
  86. package/src/vocabulary/index.ts +6 -5
  87. package/src/watch/command.ts +28 -2
  88. package/src/watch/lane-report.ts +2 -0
  89. package/src/watch/lane-rest.ts +7 -1
  90. package/src/watch/reference-paths.ts +1 -0
  91. package/src/watch/unlinked-artifacts-read.ts +43 -0
  92. package/src/watch/unlinked-artifacts.ts +76 -0
  93. package/src/watch/watch-events.ts +12 -0
  94. package/src/watch/watcher-lock.ts +47 -4
@@ -1,11 +1,9 @@
1
1
  import { phaseLabel } from '../plan-format'
2
- import { fit, paint, spread, visibleLength } from '../terminal/text'
3
- import { deeper, type Frame, joinFrames, nest, rowFrame, textFrame } from './frame'
4
- import type { ActivePhase, LaneSummary, PlacedPhase } from './lane-summary'
2
+ import { bar, fit, paint, spread, visibleLength } from '../terminal/text'
3
+ import { type Frame, indented, joinFrames, narrower, rowFrame, spaced, textFrame } from './frame'
4
+ import { type ActivePhase, type LaneSummary, type PlacedPhase, SHOWN_OPEN_TASKS } from './lane-summary'
5
5
  import {
6
6
  activeArrow,
7
- activeTitle,
8
- agentCell,
9
7
  COLUMN_SEPARATOR,
10
8
  type LaneContext,
11
9
  MIN_TITLE_WIDTH,
@@ -16,24 +14,36 @@ import {
16
14
  plain,
17
15
  subagentLine,
18
16
  TASK_INDENT,
19
- taskFrame,
17
+ type TaskStyle,
18
+ taskRowFrame,
20
19
  taskTitle,
21
20
  } from './layout-phase'
22
21
  import { type PhaseModel, phaseModelKey } from './phase-models'
23
22
  import { phaseRanges } from './phase-ranges'
24
- import { finishedKey, itemKey, nextKey, phaseKey } from './selection'
25
- import { PHASE_COUNT } from './symbols'
23
+ import { finishedKey, nextKey, phaseKey } from './selection'
24
+ import { chevronOf, PHASE_COUNT } from './symbols'
26
25
  import { type HerdTexts, TEXTS } from './texts'
27
26
 
28
27
  /** The context of a lane's branches: the lane's, with the herd's words. */
29
28
  type SummaryContext = LaneContext & { labels: HerdTexts }
30
29
 
30
+ /** What the row under a phase's tasks counts: the open tasks past the first ones and the done ones. */
31
+ type TaskTally = { more: number; done: number }
32
+
33
+ /** What an open lane shows above its phases, drawn at the width of its children: the session, its link chips and its pull requests. */
34
+ export type LaneHead = { session: Frame; chips: Frame; pullRequests: Frame }
35
+
31
36
  /** Whether only the boxes of the review gate are open. */
32
37
  const onlyGateOpen = ({ items }: ActivePhase['phase']): boolean => {
33
38
  const open = items.filter(({ done }) => !done)
34
39
  return open.length > 0 && open.every(({ gate }) => gate)
35
40
  }
36
41
 
42
+ const taskTally = ({ items }: ActivePhase['phase']): TaskTally => {
43
+ const done = items.filter((item) => item.done).length
44
+ return { more: Math.max(items.length - done - SHOWN_OPEN_TASKS, 0), done }
45
+ }
46
+
37
47
  /**
38
48
  * `· 6 min ago`, dim: when the running phase was last ticked or reported, nothing when the herd saw neither; `· gate`
39
49
  * after it while only the review gate is open. Empty when there is neither.
@@ -47,19 +57,21 @@ const ageCell = ({ minutesAgo, phase }: ActivePhase, labels: HerdTexts): string
47
57
  }
48
58
 
49
59
  /**
50
- * `P12 <title> [model] · 6 min ago 16/26`: title at the left, model, age and count at the right edge, no arrow
51
- * (arrows stand before tasks only). In a pane too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model gives
52
- * way first, then the age.
60
+ * `P12 <title> [model] · 6 min ago ████░░ 16/26`: the bold heading at the left, model, age, bar and count at the right edge,
61
+ * no arrow (arrows stand before tasks only). In a pane too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model
62
+ * gives way first, then the age, then the bar.
53
63
  */
54
- const activeRow = (active: ActivePhase, current: boolean, context: SummaryContext, model?: PhaseModel): string => {
64
+ const phaseHead = (active: ActivePhase, current: boolean, context: SummaryContext, model?: PhaseModel): string => {
55
65
  const { width, labels } = context
56
66
  const { phase } = active
57
67
  const title = phase.title === null ? paint.dim(TEXTS.notInPlan) : plain(phase.title)
58
- const head = `${phaseLabel(phase.id)} ${title}`
68
+ const label = `${phaseLabel(phase.id)} ${title}`
69
+ const head = phase.title === null ? label : paint.bold(label)
59
70
  const count = phaseCountCell(phase, current)
60
- const withAge = `${ageCell(active, labels)}${count}`
71
+ const counted = phase.total > 0 ? `${bar(phase.done, phase.total)} ${count}` : count
72
+ const withAge = `${ageCell(active, labels)}${counted}`
61
73
  const mark = modelMark(model)
62
- const candidates = [...(mark ? [`${mark}${COLUMN_SEPARATOR}${withAge}`] : []), withAge]
74
+ const candidates = [...(mark ? [`${mark}${COLUMN_SEPARATOR}${withAge}`] : []), withAge, counted]
63
75
  const titleRoom = (right: string) => width - visibleLength(head) + visibleLength(title) - visibleLength(right) - 1
64
76
  return spread(head, candidates.find((right) => titleRoom(right) >= MIN_TITLE_WIDTH) ?? count, width)
65
77
  }
@@ -68,75 +80,100 @@ const activeRow = (active: ActivePhase, current: boolean, context: SummaryContex
68
80
  const taskText = (item: ActivePhase['phase']['items'][number], labels: HerdTexts): string =>
69
81
  item.gate ? labels.reviewGate : taskTitle(item.text)
70
82
 
71
- /** The texts of the tasks agents report on the phase, then the descriptions of the subagents that name no phase as `Subagent: …`. */
72
- const reportedTexts = (
73
- { phase, tasks }: ActivePhase,
74
- subagentTasks: readonly string[],
75
- labels: HerdTexts,
76
- ): string[] => [
77
- ...tasks
83
+ /** The texts of the tasks agents report on the phase. */
84
+ const reportedTaskTexts = ({ phase, tasks }: ActivePhase, labels: HerdTexts): string[] =>
85
+ tasks
78
86
  .filter(({ guessed }) => !guessed)
79
87
  .flatMap(({ index }) => {
80
88
  const item = phase.items[index]
81
89
  return item ? [taskText(item, labels)] : []
82
- }),
90
+ })
91
+
92
+ /**
93
+ * In the order of the open lane: the tasks agents report on the phase, the reported ones no plan task names as
94
+ * `now: …`, then the descriptions of the subagents that name no phase as `Subagent: …`.
95
+ */
96
+ const reportedTexts = (
97
+ active: ActivePhase | undefined,
98
+ { unplanned, subagentTasks }: Pick<LaneSummary, 'unplanned' | 'subagentTasks'>,
99
+ labels: HerdTexts,
100
+ ): string[] => [
101
+ ...(active ? reportedTaskTexts(active, labels) : []),
102
+ ...unplanned.map(({ text }) => `${TEXTS.now} ${text}`),
83
103
  ...subagentTasks.map((description) => `${TEXTS.subagent} ${taskTitle(description)}`),
84
104
  ]
85
105
 
86
106
  /**
87
- * `▶ Name +1` for a folded lane, behind `indent`: the first task the phase is on with the arrow of the open lane, `+n`
88
- * for the others in parallel at the end; no ticks, the phase row shows them; the name cut to the pane's edge. A task
89
- * an agent reports comes first, the descriptions of the `subagentTasks` after it. Null while neither exists: a task
90
- * the herd only guesses from words is no sign that the lane works.
107
+ * `● Name +1` for a folded lane, behind `indent`: the first task the lane reports with the arrow of the open lane, its
108
+ * text drawn in `style`, `+n` for the others in parallel at the end; no ticks, the phase row shows them; the name cut
109
+ * to the pane's edge. The order is the open lane's: a task of the `active` phase, then `now: …`, then `Subagent: …`.
110
+ * Null while none exists: a task the herd only guesses from words is no sign that the lane works.
91
111
  */
92
112
  export const foldedTaskLine = (
93
- active: ActivePhase,
94
- subagentTasks: readonly string[],
113
+ active: ActivePhase | undefined,
114
+ reported: Pick<LaneSummary, 'unplanned' | 'subagentTasks'>,
95
115
  labels: HerdTexts,
96
- indent: string,
97
- width: number,
116
+ { indent, width, style }: { indent: string; width: number; style: TaskStyle },
98
117
  ): string | null => {
99
- const [first, ...others] = reportedTexts(active, subagentTasks, labels)
118
+ const [first, ...others] = reportedTexts(active, reported, labels)
100
119
  if (first === undefined) return null
101
120
  const lead = `${activeArrow(false)} `
102
121
  const tail = others.length > 0 ? ` +${others.length}` : ''
103
122
  const room = Math.max(width - visibleLength(indent) - visibleLength(lead) - visibleLength(tail), 1)
104
- return `${indent}${lead}${activeTitle(fit(first, room), false)}${tail}`
123
+ return `${indent}${lead}${style(fit(first, room))}${tail}`
124
+ }
125
+
126
+ /** The indexes of a phase's open tasks: the ones it is on first, then the others in the order of the plan. */
127
+ const openTaskOrder = ({ phase, tasks }: ActivePhase): number[] => {
128
+ const isOpen = (index: number) => phase.items[index]?.done === false
129
+ const running = tasks.map(({ index }) => index).filter(isOpen)
130
+ const others = phase.items.flatMap((item, index) => (!item.done && !running.includes(index) ? [index] : []))
131
+ return [...new Set(running), ...others]
105
132
  }
106
133
 
107
134
  /**
108
- * One row per active task under a closed running phase, each with its own arrow: the title bold and the agent dim
109
- * after it where agents reported it, `▷` and a plain title where the herd guesses. Always on one line whatever the
110
- * task's fold, and a row a click opens like the task itself; `Review gate` in place of a box of the review gate.
135
+ * The row that opens and folds a phase's tasks, dim: `▸ 2 more open · 12 done` while the phase shows its first open
136
+ * ones, the same text with `▾` once it lists them all. None while the first open tasks are all the phase has.
111
137
  */
112
- const taskLinesFrame = (context: SummaryContext, phaseRowKey: string, { phase, tasks }: ActivePhase): Frame => {
113
- return joinFrames(
114
- tasks.flatMap((task) => {
115
- const item = phase.items[task.index]
116
- if (!item) return []
117
- const text = taskText(item, context.labels)
118
- const line = spread(
119
- `${TASK_INDENT}${activeArrow(task.guessed)} ${activeTitle(text, task.guessed)}`,
120
- agentCell(task.agent),
121
- context.width,
122
- )
123
- return [taskFrame(itemKey(phaseRowKey, task.index), [line], context)]
124
- }),
125
- )
138
+ const moreTasksFrame = (key: string, tally: TaskTally, open: boolean, context: SummaryContext): Frame[] => {
139
+ if (tally.more === 0 && tally.done === 0) return []
140
+ const text = TEXTS.moreTasks(tally.more, tally.done)
141
+ return [rowFrame(key, [fit(paint.dim(`${chevronOf(open)} ${text}`), context.width)], [], context)]
126
142
  }
127
143
 
128
- /** A running phase as a row that opens to its tasks; closed, the task it is on below it. */
144
+ /** The tasks of a phase at the indexes in `shown`, each a row under `key`. */
145
+ const shownTasks = (
146
+ context: SummaryContext,
147
+ key: string,
148
+ shown: readonly number[],
149
+ active: ActivePhase,
150
+ running: ReadonlyMap<number, ActivePhase['tasks'][number]>,
151
+ ): Frame[] =>
152
+ shown.flatMap((index) => {
153
+ const item = active.phase.items[index]
154
+ return item ? [taskRowFrame(context, key, { item, index, active: running.get(index) })] : []
155
+ })
156
+
157
+ /**
158
+ * A phase the lane works on: its head with the bar, no row of its own; under it the open tasks, the ones it is on
159
+ * first, at most `SHOWN_OPEN_TASKS`, then `▸ <n> more open · <k> done`. Opened, that row stays where it stood with
160
+ * its text and a `▾`, and the rest of the tasks (`phaseChildren`) stand below it, never above.
161
+ */
129
162
  const activeFrame = (context: SummaryContext, active: ActivePhase): Frame => {
130
163
  const { progress, state } = context
131
164
  const stepIndex = active.stepIndex ?? -1
132
165
  const key = phaseKey(progress.lane.name, stepIndex, active.phase.id)
133
166
  const current = progress.current?.stepIndex === stepIndex && progress.current.phaseId === active.phase.id
134
167
  const model = context.models[phaseModelKey(stepIndex, active.phase.id)]
135
- const row = rowFrame(key, [activeRow(active, current, context, model)], [], context)
136
- if (!state.expanded.has(key)) return joinFrames([row, taskLinesFrame(context, key, active)])
168
+ const inner = narrower(context, TASK_INDENT.length)
169
+ const running = new Map(active.tasks.map((task) => [task.index, task]))
170
+ const open = state.expanded.has(key)
171
+ const shown = openTaskOrder(active).slice(0, SHOWN_OPEN_TASKS)
172
+ const more = moreTasksFrame(key, taskTally(active.phase), open, inner)
173
+ const rest = open ? phaseChildren(inner, key, active.phase, running, new Set(shown)) : []
137
174
  return joinFrames([
138
- row,
139
- nest(phaseChildren(deeper(context), key, active.phase, new Map(active.tasks.map((task) => [task.index, task])))),
175
+ textFrame([phaseHead(active, current, context, model)]),
176
+ indented(joinFrames([...shownTasks(inner, key, shown, active, running), ...more, ...rest]), TASK_INDENT),
140
177
  ])
141
178
  }
142
179
 
@@ -148,15 +185,30 @@ const unwrittenText = ({ id, missing }: LaneSummary['unwritten'][number]): strin
148
185
  * `next P5–P7, P10 P8 ? · Second plan: P1` on one line: every open phase that does not run, the current plan's as
149
186
  * ranges, then the other plans' by their name; empty when none waits.
150
187
  */
151
- const nextLineText = (summary: LaneSummary, labels: HerdTexts): string => {
188
+ const rangesText = (summary: LaneSummary): string => {
152
189
  const ownIds = summary.waiting.filter(({ planTitle }) => planTitle === null).flatMap(({ phaseIds }) => phaseIds)
153
190
  const ownRanges = phaseRanges(ownIds)
154
191
  const own = [...(ownRanges ? [ownRanges] : []), ...summary.unwritten.map(unwrittenText)]
155
192
  const others = summary.waiting
156
193
  .filter(({ planTitle }) => planTitle !== null)
157
194
  .map(({ planTitle, phaseIds }) => `${plain((planTitle ?? '').split(':')[0]).trim()}: ${phaseRanges(phaseIds)}`)
158
- const text = [...(own.length > 0 ? [own.join(' ')] : []), ...others].join(' · ')
159
- return text ? `${paint.dim(labels.next)} ${text}` : ''
195
+ return [...(own.length > 0 ? [own.join(' ')] : []), ...others].join(' · ')
196
+ }
197
+
198
+ /**
199
+ * The outlook of an open lane, `next P5 Export +2`: the first phase that waits by its title and how many wait
200
+ * after it; without a written one, every waiting phase as ranges. Empty when none waits.
201
+ */
202
+ const nextLineText = (summary: LaneSummary, labels: HerdTexts): string => {
203
+ const [first] = summary.waitingPhases
204
+ const count = summary.waiting.flatMap(({ phaseIds }) => phaseIds).length + summary.unwritten.length
205
+ if (!first) {
206
+ const ranges = rangesText(summary)
207
+ return ranges ? `${paint.dim(labels.next)}${COLUMN_SEPARATOR}${ranges}` : ''
208
+ }
209
+ const title = first.phase.title ? ` ${plain(first.phase.title)}` : ''
210
+ const more = count > 1 ? paint.dim(`${COLUMN_SEPARATOR}+${count - 1}`) : ''
211
+ return `${paint.dim(labels.next)}${COLUMN_SEPARATOR}${phaseLabel(first.phase.id)}${title}${more}`
160
212
  }
161
213
 
162
214
  /** `done P1–P7, P10` on one line: the word `done` in green, the finished phases as ranges. */
@@ -165,13 +217,13 @@ const finishedText = (summary: LaneSummary, labels: HerdTexts): string => {
165
217
  return phases ? `${paint.green(labels.done)} ${phases}` : ''
166
218
  }
167
219
 
168
- /** A phase that does not run as a row, which opens to its tasks like a running one; closed, it has no task line below it. */
220
+ /** A phase that does not run, drawn like a running one: its head, its first open tasks and the line that lists them all. */
169
221
  const restingFrame = (context: SummaryContext, { phase, stepIndex }: PlacedPhase): Frame =>
170
222
  activeFrame(context, { phase, stepIndex, minutesAgo: null, tasks: [] })
171
223
 
172
224
  /**
173
- * One line of text cut at the pane's edge, a row under `key`; no line when the text is empty. Open, it continues with
174
- * a row per phase it names and the `unwritten` lines, each phase opening to its tasks.
225
+ * `▸ <text>`: one line cut at the pane's edge, a row under `key`; no line when the text is empty. Open (`▾`), it
226
+ * continues with each phase it names and the `unwritten` lines, indented under it.
175
227
  */
176
228
  const foldFrame = (
177
229
  context: SummaryContext,
@@ -180,19 +232,17 @@ const foldFrame = (
180
232
  { phases, unwritten }: { phases: readonly PlacedPhase[]; unwritten: string[] },
181
233
  ): Frame => {
182
234
  if (!text) return textFrame([])
183
- const row = rowFrame(key, [fit(text, context.width)], [], context)
184
- if (!context.state.expanded.has(key)) return row
185
- const inner = deeper(context)
235
+ const open = context.state.expanded.has(key)
236
+ const row = rowFrame(key, [fit(`${paint.dim(chevronOf(open))} ${text}`, context.width)], [], context)
237
+ if (!open) return row
238
+ const inner = narrower(context, TASK_INDENT.length)
186
239
  const children = [
187
240
  ...phases.map((placed) => restingFrame(inner, placed)),
188
241
  ...unwritten.map((line) => textFrame([fit(line, inner.width)])),
189
242
  ]
190
- return joinFrames([row, nest(children)])
243
+ return joinFrames([row, indented(joinFrames(children), TASK_INDENT)])
191
244
  }
192
245
 
193
- /** What an open lane shows above its phases, drawn at the width of its branches: the session, its pull requests and its link chips. */
194
- export type LaneHead = { session: Frame; pullRequests: Frame; chips: Frame }
195
-
196
246
  /** The `next` and `done` rows of an open lane. */
197
247
  const restingFrames = (context: SummaryContext, summary: LaneSummary): Frame[] => {
198
248
  const laneName = context.progress.lane.name
@@ -209,21 +259,21 @@ const restingFrames = (context: SummaryContext, summary: LaneSummary): Frame[] =
209
259
  }
210
260
 
211
261
  /**
212
- * The children of an open lane, one branch each, told apart by their marks rather than by a label: the session
213
- * (`⎇`), the pull requests (`↗`), the link chips, then one branch per running phase (its tasks `▶`/`▷` under it), the
214
- * `now:` and `Subagent:` lines, `next` with the open phases that wait, and `done` with the finished ones; a branch without lines is left out.
262
+ * The children of an open lane, told apart by their marks and a blank line between the groups, not by a label or a
263
+ * tree: the session (`⎇`), the link chips and the pull requests (`↗`); each running phase with its tasks (`●`/`◌`); the
264
+ * `now:` and `Subagent:` lines; the outlook, `next` with the open phases that wait and `done` with the finished ones.
265
+ * A group without lines is left out.
215
266
  */
216
- export const summaryChildren = (context: LaneContext, summary: LaneSummary, head: LaneHead): Frame[] => {
217
- const labels = TEXTS.herd
218
- const inner = { ...context, labels }
219
- const { width } = context
220
- return [
221
- head.session,
222
- head.pullRequests,
223
- head.chips,
267
+ export const openLaneFrame = (context: LaneContext, summary: LaneSummary, head: LaneHead): Frame => {
268
+ const inner = { ...context, labels: TEXTS.herd }
269
+ const { width, taskStyle } = context
270
+ return spaced([
271
+ joinFrames([head.session, head.chips, head.pullRequests]),
224
272
  ...summary.active.map((active) => activeFrame(inner, active)),
225
- ...summary.unplanned.map((task) => textFrame([nowLine(task, width)])),
226
- ...summary.subagentTasks.map((description) => textFrame([subagentLine(description, width)])),
227
- ...restingFrames(inner, summary),
228
- ]
273
+ textFrame([
274
+ ...summary.unplanned.map((task) => nowLine(task, width, taskStyle)),
275
+ ...summary.subagentTasks.map((description) => subagentLine(description, width, taskStyle)),
276
+ ]),
277
+ joinFrames(restingFrames(inner, summary)),
278
+ ])
229
279
  }