jackherd 1.11.2 → 1.12.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.
- package/CHANGELOG.md +21 -0
- package/README.md +69 -51
- package/dist/{chunk-RMMXD4G5.js → chunk-V4HOLGFP.js} +2 -2
- package/dist/config.d.ts +10 -0
- package/dist/config.js +15 -5
- package/dist/plan-format.d.ts +11 -6
- package/dist/plan-format.js +8 -2
- package/docs/follow-dark.svg +2 -2
- package/docs/follow.svg +2 -2
- package/docs/workflow-dark.svg +56 -64
- package/docs/workflow.svg +56 -64
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/workflow/skills/implement/SKILL.md +1 -1
- package/kit/workflow/skills/orchestrate/SKILL.md +1 -1
- package/kit/workflow/skills/orchestrate/references/draft-review.md +4 -0
- package/kit/workflow/skills/orchestrate/references/events.md +8 -1
- package/kit/workflow/skills/plan/SKILL.md +2 -0
- package/package.json +2 -2
- package/schemas/jackherd.schema.json +18 -1
- package/src/config/link-kinds.ts +1 -1
- package/src/config/schema.ts +5 -2
- package/src/config/tones.ts +9 -2
- package/src/context/fill.ts +4 -10
- package/src/context/session-log.ts +108 -2
- package/src/plan-format/lanes.ts +10 -1
- package/src/plan-format/phases.ts +11 -3
- package/src/plan-format/progress.ts +4 -2
- package/src/plan-format/waiting.ts +12 -0
- package/src/plans/active-tasks.ts +31 -8
- package/src/plans/appointment-rows.ts +42 -53
- package/src/plans/board-rows.ts +14 -0
- package/src/plans/calendar-days.ts +29 -0
- package/src/plans/click-hint.ts +94 -0
- package/src/plans/command.ts +8 -9
- package/src/plans/current-phase.ts +22 -10
- package/src/plans/current-task.ts +18 -3
- package/src/plans/frame.ts +18 -26
- package/src/plans/git.ts +90 -31
- package/src/plans/groomed.ts +13 -7
- package/src/plans/lane-reason.ts +61 -0
- package/src/plans/lane-summary.ts +75 -24
- package/src/plans/layout-phase.ts +123 -76
- package/src/plans/layout-summary.ts +133 -83
- package/src/plans/layout.ts +451 -345
- package/src/plans/legend.ts +29 -0
- package/src/plans/link-chips.ts +26 -8
- package/src/plans/live.ts +238 -84
- package/src/plans/next-rows.ts +5 -37
- package/src/plans/phase-source.ts +8 -3
- package/src/plans/plan-chips.ts +6 -3
- package/src/plans/plan-heads.ts +1 -1
- package/src/plans/planned.ts +4 -1
- package/src/plans/process-slots.ts +36 -0
- package/src/plans/pull-requests.ts +9 -1
- package/src/plans/read-cache.ts +18 -3
- package/src/plans/refresh-pace.ts +13 -0
- package/src/plans/row-focus.ts +86 -15
- package/src/plans/selection.ts +124 -89
- package/src/plans/sent-files.ts +1 -1
- package/src/plans/settings.ts +5 -2
- package/src/plans/shimmer.ts +124 -0
- package/src/plans/signals-read.ts +38 -10
- package/src/plans/slice-gate.ts +51 -0
- package/src/plans/snapshot.ts +185 -84
- package/src/plans/source-asked.ts +1 -1
- package/src/plans/source-rows.ts +60 -18
- package/src/plans/sprint-html.ts +1 -1
- package/src/plans/status-pace.ts +45 -0
- package/src/plans/subagent-tasks.ts +1 -1
- package/src/plans/subagents-read.ts +61 -24
- package/src/plans/subagents.ts +2 -2
- package/src/plans/symbols.ts +4 -1
- package/src/plans/task-brief.ts +143 -0
- package/src/plans/texts.ts +181 -39
- package/src/plans/view.ts +198 -32
- package/src/plans/waiting-click.ts +54 -110
- package/src/plans/watch-burst.ts +25 -0
- package/src/plans/worktree-stamp.ts +35 -0
- package/src/plans/your-turn-rows.ts +102 -0
- package/src/plans/your-turn.ts +226 -0
- package/src/terminal/herdr.ts +25 -5
- package/src/terminal/restore.ts +44 -0
- package/src/terminal/text.ts +2 -2
- package/src/terminal/width.ts +9 -0
- package/src/vocabulary/index.ts +6 -5
- package/src/watch/command.ts +28 -2
- package/src/watch/lane-report.ts +2 -0
- package/src/watch/lane-rest.ts +7 -1
- package/src/watch/reference-paths.ts +1 -0
- package/src/watch/unlinked-artifacts-read.ts +43 -0
- package/src/watch/unlinked-artifacts.ts +76 -0
- package/src/watch/watch-events.ts +12 -0
- 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 {
|
|
4
|
-
import type
|
|
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
|
-
|
|
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,
|
|
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
|
|
51
|
-
* (arrows stand before tasks only). In a pane too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model
|
|
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
|
|
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
|
|
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
|
|
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
|
|
72
|
-
const
|
|
73
|
-
|
|
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
|
-
*
|
|
88
|
-
* for the others in parallel at the end; no ticks, the phase row shows them; the name cut
|
|
89
|
-
*
|
|
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
|
-
|
|
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,
|
|
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}${
|
|
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
|
-
*
|
|
109
|
-
*
|
|
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
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
/**
|
|
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
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
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
|
|
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
|
-
*
|
|
174
|
-
*
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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,
|
|
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,
|
|
213
|
-
*
|
|
214
|
-
* `now:` and `Subagent:` lines, `next` with the open phases that wait
|
|
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
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
}
|