jackherd 1.9.0 → 1.10.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/CHANGELOG.md +30 -0
- package/INSTALL.md +1 -1
- package/README.md +50 -32
- package/dist/{chunk-MCP4XJQH.js → chunk-5MQBCUQ5.js} +9 -4
- package/dist/config.d.ts +39 -4
- package/dist/config.js +67 -26
- package/dist/plan-format.d.ts +45 -6
- package/dist/plan-format.js +46 -6
- package/docs/workflow-dark.svg +67 -63
- package/docs/workflow.svg +67 -63
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/ask-rules.json +15 -0
- package/kit/hooks/hooks/ask-hint/decision.ts +82 -14
- package/kit/hooks/hooks/ask-hint/index.ts +15 -19
- package/kit/hooks/hooks/ask-hint/settings.ts +3 -18
- package/kit/hooks/hooks/git-guard/decision.ts +50 -102
- package/kit/hooks/hooks/merge-guard/always-ask.ts +63 -0
- package/kit/hooks/hooks/merge-guard/{plan-approval-settings.ts → board-settings.ts} +21 -67
- package/kit/hooks/hooks/merge-guard/check-free.ts +20 -0
- package/kit/hooks/hooks/merge-guard/decision.ts +127 -187
- package/kit/hooks/hooks/merge-guard/index.ts +6 -3
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +79 -0
- package/kit/hooks/hooks/merge-guard/run.ts +59 -54
- package/kit/hooks/hooks/merge-guard/settings.ts +13 -1
- package/kit/hooks/hooks/merge-guard/standing-approval.ts +142 -0
- package/kit/hooks/hooks/open-question/index.ts +26 -0
- package/kit/hooks/hooks/open-question/record.ts +135 -0
- package/kit/hooks/hooks/open-question/run.ts +52 -0
- package/kit/hooks/hooks/open-question/store.ts +39 -0
- package/kit/hooks/hooks/sent-file/store.ts +3 -12
- package/kit/hooks/hooks/shared/config.ts +7 -0
- package/kit/hooks/hooks/shared/dialog-answers.ts +9 -9
- package/kit/hooks/hooks/shared/files.ts +7 -0
- package/kit/hooks/hooks/shared/lane.ts +1 -1
- package/kit/hooks/hooks/shared/newest-verdict.ts +10 -0
- package/kit/hooks/hooks/shared/private-directory.ts +56 -9
- package/kit/hooks/hooks/shared/program-position.ts +2 -75
- package/kit/hooks/hooks/shared/shell-aliases.ts +1 -71
- package/kit/hooks/hooks/{ask-hint → shared}/transcript.ts +37 -6
- package/kit/hooks/hooks/{merge-guard → shared}/xargs-program.ts +10 -8
- package/kit/hooks/hooks/task-hint/decision.ts +49 -0
- package/kit/hooks/hooks/task-hint/index.ts +79 -0
- package/kit/hooks/hooks/task-hint/run.ts +62 -0
- package/kit/hooks/hooks/task-hint/store.ts +51 -0
- package/kit/hooks/hooks.json +13 -5
- package/kit/review/agents/skill-review.md +34 -0
- package/kit/review/skills/pre-commit/SKILL.md +1 -1
- package/kit/workflow/skills/follow/SKILL.md +1 -8
- package/kit/workflow/skills/implement/SKILL.md +8 -6
- package/kit/workflow/skills/orchestrate/SKILL.md +210 -433
- package/kit/workflow/skills/orchestrate/handover/decision.ts +43 -8
- package/kit/workflow/skills/orchestrate/handover/index.ts +32 -4
- package/kit/workflow/skills/orchestrate/handover/standing-approval.ts +84 -0
- package/kit/workflow/skills/orchestrate/references/acceptance.md +15 -0
- package/kit/workflow/skills/orchestrate/references/close-lane.md +17 -0
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +43 -17
- package/kit/workflow/skills/orchestrate/references/draft-review.md +19 -0
- package/kit/workflow/skills/orchestrate/references/events.md +86 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +6 -6
- package/kit/workflow/skills/orchestrate/references/lane-handover.md +18 -0
- package/kit/workflow/skills/orchestrate/references/lane-session.md +25 -0
- package/kit/workflow/skills/orchestrate/references/lane-setup.md +37 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -10
- package/kit/workflow/skills/orchestrate/references/sensitive-input.md +5 -7
- package/kit/workflow/skills/plan/SKILL.md +6 -5
- package/kit/workflow/skills/plan/references/template-English.md +4 -4
- package/kit/workflow/skills/plan/references/template-German.md +4 -4
- package/package.json +3 -1
- package/recipes/README.md +2 -1
- package/recipes/sources.md +1 -1
- package/schemas/jackherd.schema.json +59 -5
- package/src/check/agent-text-reviewers.ts +23 -0
- package/src/check/command.ts +2 -0
- package/src/check/plan-structure-check.ts +49 -2
- package/src/check/plan.ts +8 -1
- package/src/check/schema.ts +9 -0
- package/src/check/skill-budget-defaults.ts +55 -0
- package/src/check/skill-budgets.ts +185 -0
- package/src/cli.ts +1 -0
- package/src/config/auto-window.ts +8 -0
- package/src/config/link-kinds.ts +7 -1
- package/src/config/schema.ts +58 -24
- package/src/lane/close-board.ts +106 -0
- package/src/lane/close-check.ts +183 -0
- package/src/lane/close-command-line.ts +11 -0
- package/src/lane/close-command.ts +217 -0
- package/src/lane/close-options.ts +43 -0
- package/src/lane/close-survey.ts +83 -0
- package/src/lane/close.ts +141 -0
- package/src/lane/command.ts +13 -0
- package/src/lane-prompt/build.ts +74 -0
- package/src/lane-prompt/command.ts +87 -0
- package/src/lane-prompt/input.ts +116 -0
- package/src/lane-prompt/options.ts +68 -0
- package/src/lane-prompt/texts.ts +101 -0
- package/src/plan-format/clock.ts +8 -0
- package/src/plan-format/dependencies.ts +7 -4
- package/src/plan-format/index.ts +1 -0
- package/src/plan-format/short-names.ts +71 -0
- package/src/plan-format/waiting.ts +7 -2
- package/src/plans/appointment-rows.ts +59 -0
- package/src/plans/ask-coordinator.ts +77 -0
- package/src/plans/ask-target.ts +25 -0
- package/src/plans/child-stop.ts +51 -0
- package/src/plans/closable-records.ts +34 -0
- package/src/plans/command.ts +3 -2
- package/src/plans/fetch.ts +26 -3
- package/src/plans/git.ts +19 -5
- package/src/plans/herd-claim.ts +173 -0
- package/src/plans/herd-memory.ts +36 -25
- package/src/plans/lane-plans.ts +44 -13
- package/src/plans/lane-summary.ts +253 -45
- package/src/plans/layout-phase.ts +29 -7
- package/src/plans/layout-summary.ts +89 -18
- package/src/plans/layout.ts +262 -68
- package/src/plans/link-chips.ts +1 -0
- package/src/plans/live.ts +107 -20
- package/src/plans/phase-ids.ts +8 -0
- package/src/plans/phase-source.ts +15 -0
- package/src/plans/plan-cache.ts +28 -0
- package/src/plans/plan-heads.ts +54 -0
- package/src/plans/pull-requests.ts +17 -2
- package/src/plans/question-records.ts +48 -0
- package/src/plans/questions-read.ts +13 -0
- package/src/plans/read-cache.ts +8 -0
- package/src/plans/record-id.ts +4 -0
- package/src/plans/restart-decision.ts +13 -4
- package/src/plans/selection.ts +98 -18
- package/src/plans/sent-files-read.ts +6 -15
- package/src/plans/sent-files.ts +7 -14
- package/src/plans/settings.ts +2 -1
- package/src/plans/signalled-phase.ts +42 -0
- package/src/plans/signals-read.ts +12 -4
- package/src/plans/snapshot.ts +85 -62
- package/src/plans/sprint-export.ts +11 -11
- package/src/plans/sprint-page.ts +5 -5
- package/src/plans/subagent-phases.ts +95 -0
- package/src/plans/subagent-tasks.ts +51 -0
- package/src/plans/subagents-read.ts +66 -17
- package/src/plans/subagents.ts +64 -6
- package/src/plans/supervisor.ts +14 -4
- package/src/plans/symbols.ts +3 -0
- package/src/plans/task-name.ts +34 -0
- package/src/plans/texts.ts +23 -6
- package/src/plans/view.ts +17 -9
- package/src/plans/waiting-click.ts +192 -37
- package/src/plans/waiting-records.ts +39 -0
- package/src/plans/waiting-window.ts +8 -0
- package/src/preview/command.ts +25 -3
- package/src/shared/private-location.ts +89 -0
- package/src/shared/stat-cache.ts +48 -0
- package/src/sync/kit-source.ts +29 -10
- package/src/sync/kit.ts +2 -0
- package/src/sync/manifest.ts +2 -0
- package/src/sync/plan-ask-rules.ts +94 -0
- package/src/sync/plan-hooks.ts +2 -0
- package/src/sync/render.ts +3 -3
- package/src/sync/settings-hooks.ts +4 -1
- package/src/sync/sync-plan.ts +1 -0
- package/src/sync/sync.ts +24 -3
- package/src/task-signals/claude-read.ts +27 -10
- package/src/terminal/text.ts +8 -3
- package/src/version/skill-diff.ts +46 -0
- package/src/vocabulary/index.ts +16 -6
- package/src/waiting/command.ts +22 -7
- package/src/watch/appointments.ts +1 -2
- package/src/watch/asking-lanes.ts +40 -0
- package/src/watch/base-tip.ts +11 -0
- package/src/watch/command.ts +330 -105
- package/src/watch/heartbeat-progress.ts +6 -18
- package/src/watch/kit-update.ts +28 -9
- package/src/watch/lane-name.ts +6 -0
- package/src/watch/lane-progress.ts +40 -8
- package/src/watch/lane-report-file.ts +20 -5
- package/src/watch/lane-report.ts +32 -4
- package/src/watch/lane-sources.ts +31 -3
- package/src/watch/main-checkout.ts +34 -0
- package/src/watch/plan-badges-read.ts +13 -7
- package/src/watch/pull-requests.ts +6 -2
- package/src/watch/reference-paths.ts +87 -0
- package/src/watch/running-subagents.ts +43 -0
- package/src/watch/skill-change.ts +124 -32
- package/src/watch/source-change.ts +48 -0
- package/src/watch/sources.ts +6 -3
- package/src/watch/stream-output.ts +4 -3
- package/src/watch/task-parallel-auto.ts +208 -0
- package/src/watch/task-parallel-data-paths.ts +46 -0
- package/src/watch/task-parallel-input.ts +5 -15
- package/src/watch/task-parallel-read.ts +3 -1
- package/src/watch/task-parallel.ts +36 -7
- package/src/watch/task-signal-missing.ts +69 -0
- package/src/watch/task-signal-read.ts +29 -0
- package/src/watch/texts.ts +10 -0
- package/src/watch/update-available.ts +6 -3
- package/src/watch/watch-events.ts +23 -0
- package/src/watch/watcher-lock.ts +25 -29
- package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +0 -23
- package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +0 -196
- package/kit/hooks/hooks/merge-guard/plan-approval.ts +0 -168
- package/kit/hooks/hooks/merge-guard/plan-growth.ts +0 -40
- package/kit/hooks/hooks/merge-guard/plan-pointers.ts +0 -194
- package/kit/hooks/hooks/shared/command-text-runners.ts +0 -71
- package/kit/hooks/hooks/shared/plan-open-phases.ts +0 -114
package/src/plans/layout.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type WatchHealth,
|
|
29
29
|
} from '../watch/plan-badges'
|
|
30
30
|
import type { UnplannedTask } from './active-tasks'
|
|
31
|
+
import { type AppointmentBlock, appointmentItems, appointmentKeys } from './appointment-rows'
|
|
31
32
|
import { deeper, type Frame, joinFrames, nest, prefixed, rowFrame, TREE, textFrame } from './frame'
|
|
32
33
|
import { LANE_STANDING, type LaneStanding, laneStanding, waitingNote } from './lane-standing'
|
|
33
34
|
import { type LaneSummary, laneSummary } from './lane-summary'
|
|
@@ -41,25 +42,40 @@ import {
|
|
|
41
42
|
nowLine,
|
|
42
43
|
plain,
|
|
43
44
|
} from './layout-phase'
|
|
44
|
-
import { summaryChildren } from './layout-summary'
|
|
45
|
+
import { foldedTaskLine, summaryChildren } from './layout-summary'
|
|
45
46
|
import { chipKey, type LinkChip, planUrlOf } from './link-chips'
|
|
46
47
|
import type { PhaseModel } from './phase-models'
|
|
47
48
|
import { type PlannedPlan, phaseList, plannedPhaseCount } from './planned'
|
|
48
49
|
import { CHECKS, type ChecksState, failedTestKey, type PullRequest, pullRequestKey } from './pull-requests'
|
|
49
50
|
import {
|
|
50
51
|
type ClickTarget,
|
|
52
|
+
isBlockFolded,
|
|
51
53
|
isLaneOpen,
|
|
52
54
|
laneKey,
|
|
53
55
|
PLANNED_KEY,
|
|
54
56
|
type Selection,
|
|
55
57
|
selectedKey,
|
|
58
|
+
TOP_BLOCK,
|
|
59
|
+
type TopBlockId,
|
|
56
60
|
targetKeys,
|
|
57
61
|
} from './selection'
|
|
58
62
|
import type { SentChip } from './sent-files'
|
|
59
63
|
import { shownWithMore } from './shown-with-more'
|
|
60
|
-
import { chevronOf, NO_AGENT_SYMBOL, statusSymbol, statusWord } from './symbols'
|
|
64
|
+
import { chevronOf, LESS_MARK, NO_AGENT_SYMBOL, statusSymbol, statusWord } from './symbols'
|
|
61
65
|
import { BADGE_TEXTS, type BadgeTexts, checksText, laneMarkText, TEXTS } from './texts'
|
|
62
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
APPOINTMENT_MORE_KEY,
|
|
68
|
+
ASK_BUTTON,
|
|
69
|
+
type BlockHit,
|
|
70
|
+
type BlockRow,
|
|
71
|
+
blockRows,
|
|
72
|
+
moreKeyOf,
|
|
73
|
+
SHOWN_BLOCK_ROWS,
|
|
74
|
+
WAITING_MARK,
|
|
75
|
+
WAITING_MORE_KEY,
|
|
76
|
+
waitingKeys,
|
|
77
|
+
waitingRows,
|
|
78
|
+
} from './waiting-click'
|
|
63
79
|
|
|
64
80
|
/**
|
|
65
81
|
* Where a lane stands outside its plan. `agent` is the terminal adapter's status, null when no agent is listed;
|
|
@@ -105,13 +121,24 @@ export type OverviewView = {
|
|
|
105
121
|
watchesTickets?: boolean
|
|
106
122
|
/** The open items of the board's section of what waits on the user, shown under the title. */
|
|
107
123
|
awaiting?: WaitingItem[]
|
|
124
|
+
/** The open appointments of the board, in a block of their own under the for-you block. */
|
|
125
|
+
appointments?: AppointmentBlock
|
|
108
126
|
/** What agents report doing that no lane takes, one `now:` line each under the title. */
|
|
109
127
|
unplaced?: UnplannedTask[]
|
|
110
128
|
}
|
|
111
129
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
/**
|
|
131
|
+
* The drawn screen: a fixed header and footer around a body that scrolls; body lines and targets count from the body's
|
|
132
|
+
* top. `waitingBlock` and `appointmentBlock` are what a click on the for-you and the appointment block is matched
|
|
133
|
+
* against, null while the block is not drawn.
|
|
134
|
+
*/
|
|
135
|
+
export type Screen = {
|
|
136
|
+
header: string[]
|
|
137
|
+
body: Frame
|
|
138
|
+
footer: string[]
|
|
139
|
+
waitingBlock: BlockHit | null
|
|
140
|
+
appointmentBlock: BlockHit | null
|
|
141
|
+
}
|
|
115
142
|
|
|
116
143
|
/**
|
|
117
144
|
* Widths shared by every lane line: the name column, the folded pull request badges, and bar with context. They are
|
|
@@ -121,8 +148,32 @@ export type Screen = { header: string[]; body: Frame; footer: string[]; waitingL
|
|
|
121
148
|
*/
|
|
122
149
|
type LaneColumns = { name: number; marks: number; right: number; phases: boolean; tally: number }
|
|
123
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The lines of a block on top of the herd; the head of an open one is a rule of its own, a folded one is a plain line.
|
|
153
|
+
* `hit` is what a click on it is matched against, without the screen line the stack gives it.
|
|
154
|
+
*/
|
|
155
|
+
type BlockLines = { open: boolean; lines: string[]; hit: Omit<BlockHit, 'firstLine'> }
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* What a block of the top of the herd is drawn from: its name, the bar of its lines, the paint of the mark that starts
|
|
159
|
+
* a row, the key of its `+<n>` line, its points and their rows.
|
|
160
|
+
*/
|
|
161
|
+
type TopBlock = {
|
|
162
|
+
name: string
|
|
163
|
+
bar: () => string
|
|
164
|
+
markPaint: (text: string) => string
|
|
165
|
+
/** The paint of the count in the head while the block has points: amber for what waits on the user. */
|
|
166
|
+
countPaint: (text: string) => string
|
|
167
|
+
moreKey: string
|
|
168
|
+
expanded: boolean
|
|
169
|
+
keys: readonly string[]
|
|
170
|
+
rows: BlockRow[]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The blocks on top of the herd one under the other: their lines below the title, and the hit of each drawn block. */
|
|
174
|
+
type BlockStack = { lines: string[]; hits: (BlockHit | null)[] }
|
|
175
|
+
|
|
124
176
|
const BRANCH_MARK = '⎇'
|
|
125
|
-
const WOOF = 'woof'
|
|
126
177
|
const SENT_MARK = '📎'
|
|
127
178
|
const SYMBOL_WIDTH = 2
|
|
128
179
|
const NAME_WIDTH_MAX = 24
|
|
@@ -140,7 +191,7 @@ const COLUMN_RULE = '│'
|
|
|
140
191
|
/** The number badge of a three-digit pull request (`↗ #` and the digits, a cell of padding either side); a longer one widens it. */
|
|
141
192
|
const PULL_REQUEST_BADGE_WIDTH = 8
|
|
142
193
|
/**
|
|
143
|
-
* Under the lane line's status symbol: where the pull request row and its badge line start
|
|
194
|
+
* Under the lane line's status symbol: where the pull request row and its badge line start, and where the trunk
|
|
144
195
|
* of an open lane hangs, not under the fold arrow.
|
|
145
196
|
*/
|
|
146
197
|
const UNDER_STATUS = ' '
|
|
@@ -189,12 +240,11 @@ const aheadLabel = ({ branch, ahead }: LaneStatus): string => {
|
|
|
189
240
|
return ahead > 0 ? paint.teal(`↑${ahead}`) : ''
|
|
190
241
|
}
|
|
191
242
|
|
|
243
|
+
const branchLabel = (status: LaneStatus): string =>
|
|
244
|
+
status.branch ? paint.dim(`${BRANCH_MARK} ${status.branch}`) : paint.dim(TEXTS.noWorktree)
|
|
245
|
+
|
|
192
246
|
const gitLabel = (status: LaneStatus): string =>
|
|
193
|
-
[
|
|
194
|
-
status.branch ? paint.dim(`${BRANCH_MARK} ${status.branch}`) : paint.dim(TEXTS.noWorktree),
|
|
195
|
-
aheadLabel(status),
|
|
196
|
-
status.dirty ? paint.magenta(`±${status.dirty}`) : '',
|
|
197
|
-
]
|
|
247
|
+
[branchLabel(status), aheadLabel(status), status.dirty ? paint.magenta(`±${status.dirty}`) : '']
|
|
198
248
|
.filter(Boolean)
|
|
199
249
|
.join(' ')
|
|
200
250
|
|
|
@@ -247,7 +297,7 @@ const checksColumnOf = (pullRequest: PullRequest): number =>
|
|
|
247
297
|
/**
|
|
248
298
|
* `⟦↗ #<number>⟧ ✓ checks ⟦🌐 preview⟧ ⟦💬 3 new⟧ title`: the number as a badge and the check state in fixed
|
|
249
299
|
* columns, the watcher's badges after them and the title last, cut with `…` on its one line. When the badges leave no
|
|
250
|
-
* room for a title, they go on lines of their own below, starting in the column of the lane's status symbol
|
|
300
|
+
* room for a title, they go on lines of their own below, starting in the column of the lane's status symbol.
|
|
251
301
|
*/
|
|
252
302
|
const pullRequestRow = (pullRequest: PullRequest, context: LaneContext): Frame => {
|
|
253
303
|
const number = numberCell(pullRequest)
|
|
@@ -474,7 +524,7 @@ const chipCells = (chips: LinkChip[], tickets: LaneTickets | null, sent: readonl
|
|
|
474
524
|
|
|
475
525
|
/**
|
|
476
526
|
* `🌐 preview` opening the preview, `💬 <n> new` in amber opening the pull request, and `✋ changes requested` in dark
|
|
477
|
-
* red opening it too, each only when there is one
|
|
527
|
+
* red opening it too, each only when there is one.
|
|
478
528
|
*/
|
|
479
529
|
const badgeCells = (
|
|
480
530
|
pullRequest: PullRequest,
|
|
@@ -494,15 +544,17 @@ const badgeCells = (
|
|
|
494
544
|
}
|
|
495
545
|
|
|
496
546
|
/**
|
|
497
|
-
* The phase the lane line names: the
|
|
498
|
-
* ticked to the end, whose state the `✓` line says
|
|
547
|
+
* The phase the lane line names: the one its report or branch signals, else the open phase moved last, else the
|
|
548
|
+
* lane's current phase while it is open; never one ticked to the end, whose state the `✓` line says. Null when there
|
|
549
|
+
* is none.
|
|
499
550
|
*/
|
|
500
551
|
const headPhase = (view: LaneView): PhaseProgress | null => {
|
|
552
|
+
const { signalled, latest } = summaryOf(view)
|
|
501
553
|
const current = currentPhase(view.progress)
|
|
502
|
-
return
|
|
554
|
+
return signalled ?? latest ?? (current && isPhaseOpen(current) ? current : null)
|
|
503
555
|
}
|
|
504
556
|
|
|
505
|
-
/** `P9
|
|
557
|
+
/** `P9 Release 1.0` for the phase the lane line names; empty when there is none. */
|
|
506
558
|
const nextPhaseLabel = (view: LaneView): string => {
|
|
507
559
|
const phase = headPhase(view)
|
|
508
560
|
return phase ? `${phaseLabel(phase.id)}${phase.title ? ` ${plain(phase.title)}` : ''}` : ''
|
|
@@ -590,7 +642,7 @@ const laneRight = (view: LaneView, tallyWidth: number): string => {
|
|
|
590
642
|
/** The cells of the phase count column, 0 when the pane leaves it out. */
|
|
591
643
|
const tallyWidthOf = ({ phases, tally }: LaneColumns): number => (phases ? tally : 0)
|
|
592
644
|
|
|
593
|
-
/** A folded lane's marks in the column of `columns.marks`, right before context and bar
|
|
645
|
+
/** A folded lane's marks in the column of `columns.marks`, right before context and bar. */
|
|
594
646
|
const markedRight = (view: LaneView, columns: LaneColumns): string => {
|
|
595
647
|
const right = laneRight(view, tallyWidthOf(columns))
|
|
596
648
|
const marks = laneMarks(view)
|
|
@@ -602,7 +654,7 @@ const markedRight = (view: LaneView, columns: LaneColumns): string => {
|
|
|
602
654
|
|
|
603
655
|
/**
|
|
604
656
|
* Before a lane's detail on a line of its own: blanks up to the column of the lane's name, so a folded lane's title
|
|
605
|
-
* starts where its name does
|
|
657
|
+
* starts where its name does, as the branches of an open lane do; an open lane keeps its trunk in front.
|
|
606
658
|
*/
|
|
607
659
|
const detailIndent = (open: boolean, nameColumn: number): string => {
|
|
608
660
|
if (!open) return ' '.repeat(nameColumn)
|
|
@@ -610,19 +662,30 @@ const detailIndent = (open: boolean, nameColumn: number): string => {
|
|
|
610
662
|
return `${paint.dim(trunk)}${' '.repeat(Math.max(nameColumn - visibleLength(trunk), 0))}`
|
|
611
663
|
}
|
|
612
664
|
|
|
665
|
+
/** The task row below a folded lane's phase row, under the lane's name; none while the phase has no task on it. */
|
|
666
|
+
const foldedTaskRow = (view: LaneView, indent: string, width: number): string[] => {
|
|
667
|
+
const phase = headPhase(view)
|
|
668
|
+
const { active: running, subagentTasks } = summaryOf(view)
|
|
669
|
+
const active = running.find((each) => each.phase.id === phase?.id)
|
|
670
|
+
const line = active && foldedTaskLine(active, subagentTasks, TEXTS.herd, indent, width)
|
|
671
|
+
return line ? [line] : []
|
|
672
|
+
}
|
|
673
|
+
|
|
613
674
|
const laneLines = (view: LaneView, open: boolean, width: number, columns: LaneColumns): string[] => {
|
|
614
675
|
const { progress, status } = view
|
|
615
676
|
const cells = padVisible(paint.bold(fit(progress.lane.name, columns.name)), columns.name)
|
|
616
677
|
const head = `${paint.dim(chevronOf(open))} ${symbolCell(status.agent)} ${cells}`
|
|
617
678
|
const right = open ? laneProgressColumn(view, tallyWidthOf(columns)) : markedRight(view, columns)
|
|
618
679
|
const nameColumn = visibleLength(head) - visibleLength(cells)
|
|
619
|
-
|
|
680
|
+
const lines = withDetail(head, laneDetail(view, open), right, width, detailIndent(open, nameColumn))
|
|
681
|
+
return open ? lines : [...lines, ...foldedTaskRow(view, ' '.repeat(nameColumn), width)]
|
|
620
682
|
}
|
|
621
683
|
|
|
622
684
|
/** The parts of a session line: the session's title (the branch without one) at the left, the branch and the context value. */
|
|
623
|
-
const sessionParts = (view: LaneView): { left: string; git: string; context: string } => {
|
|
685
|
+
const sessionParts = (view: LaneView): { left: string; git: string; branch: string; context: string } => {
|
|
624
686
|
const git = gitLabel(view.status)
|
|
625
|
-
|
|
687
|
+
const branch = branchLabel(view.status)
|
|
688
|
+
return { left: view.sessionTitle ? plain(view.sessionTitle) : git, git, branch, context: contextValue(view.context) }
|
|
626
689
|
}
|
|
627
690
|
|
|
628
691
|
/**
|
|
@@ -631,31 +694,36 @@ const sessionParts = (view: LaneView): { left: string; git: string; context: str
|
|
|
631
694
|
* title cut. Without a title the branch stands at the left.
|
|
632
695
|
*/
|
|
633
696
|
const sessionLine = (view: LaneView, width: number): string => {
|
|
634
|
-
const { left, git, context } = sessionParts(view)
|
|
697
|
+
const { left, git, branch, context } = sessionParts(view)
|
|
635
698
|
if (!view.sessionTitle) return spread(fit(git, Math.max(width - CONTEXT_CELL_WIDTH, 1)), context, width)
|
|
636
699
|
const room = width - (context ? CONTEXT_CELL_WIDTH : 0)
|
|
637
|
-
const
|
|
638
|
-
|
|
700
|
+
const fitting = [git, branch].find(
|
|
701
|
+
(label) => visibleLength(left) + COLUMN_SEPARATOR.length + visibleLength(label) <= room,
|
|
702
|
+
)
|
|
703
|
+
const right = [fitting ?? '', context].filter(Boolean).join(COLUMN_SEPARATOR)
|
|
639
704
|
return spread(fit(left, Math.max(room, 1)), right, width)
|
|
640
705
|
}
|
|
641
706
|
|
|
642
707
|
/**
|
|
643
708
|
* The session line with the lane's one link chip on it, so the lane needs no row of chips: title, chip, branch and
|
|
644
|
-
* context
|
|
645
|
-
* the
|
|
709
|
+
* context. When the line is short the ahead and dirty counts of the branch go first, then the title is cut with `…`;
|
|
710
|
+
* without a title the branch stands at the left and the chip at the right. Null when even the cut title would be
|
|
711
|
+
* narrower than `MIN_TITLE_WIDTH`, and the chip keeps its row of chips.
|
|
646
712
|
*/
|
|
647
713
|
const sessionWithChip = (view: LaneView, chip: BadgeCell, width: number): Frame | null => {
|
|
648
|
-
const { left, git, context } = sessionParts(view)
|
|
714
|
+
const { left, git, branch, context } = sessionParts(view)
|
|
649
715
|
const tails = view.sessionTitle
|
|
650
716
|
? [
|
|
651
717
|
[chip.cell, git, context],
|
|
652
|
-
[chip.cell, context],
|
|
718
|
+
[chip.cell, branch, context],
|
|
653
719
|
]
|
|
654
720
|
: [[chip.cell, context]]
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
721
|
+
const candidates = tails.map((tail) => tail.filter(Boolean).join(COLUMN_SEPARATOR))
|
|
722
|
+
const roomFor = (candidate: string) => width - COLUMN_SEPARATOR.length - visibleLength(candidate)
|
|
723
|
+
const narrowest = candidates[candidates.length - 1] ?? ''
|
|
724
|
+
const cuts = Boolean(view.sessionTitle) && roomFor(narrowest) >= MIN_TITLE_WIDTH
|
|
725
|
+
const right = candidates.find((candidate) => visibleLength(left) <= roomFor(candidate)) ?? (cuts ? narrowest : null)
|
|
726
|
+
if (right === null) return null
|
|
659
727
|
const from = width - visibleLength(right)
|
|
660
728
|
const targets = chip.key === null ? [] : [{ line: 0, from, to: from + visibleLength(chip.cell), key: chip.key }]
|
|
661
729
|
return { lines: [spread(left, right, width)], cursorLine: -1, targets }
|
|
@@ -782,32 +850,153 @@ const plannedRows = (planned: PlannedPlan[], width: number, indent: string): str
|
|
|
782
850
|
|
|
783
851
|
const rule = (width: number): string => paint.dim(RULE.repeat(width))
|
|
784
852
|
|
|
785
|
-
/** The
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
853
|
+
/** The amber bar that starts every line of the for-you block: the one signal that this waits on the user. */
|
|
854
|
+
const waitingBar = (): string => STATUS_PAINT[AGENT_STATUS.blocked](WAITING_MARK)
|
|
855
|
+
|
|
856
|
+
/** The grey bar of the appointment block: it waits on nobody's answer. */
|
|
857
|
+
const appointmentBar = (): string => paint.dim(WAITING_MARK)
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* The head of an open block, `▾ <name> · <n> ──`, filled with the rule to the width, so it is the rule above the block.
|
|
861
|
+
* The label and the count wear the normal colour (the count in `countPaint`), only the rule is dim.
|
|
862
|
+
*/
|
|
863
|
+
const blockHead = (name: string, count: number, width: number, countPaint: (text: string) => string): string => {
|
|
864
|
+
const lead = `${chevronOf(true)} ${name} ${ITEM_SEPARATOR} `
|
|
865
|
+
const used = textWidth(`${lead}${count} `)
|
|
866
|
+
return `${lead}${countPaint(String(count))} ${paint.dim(RULE.repeat(Math.max(width - used, 0)))}`
|
|
789
867
|
}
|
|
790
868
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
869
|
+
const markedLine = (line: string, row: BlockRow, markPaint: (text: string) => string): string =>
|
|
870
|
+
line.startsWith(row.mark) ? `${markPaint(row.mark)}${line.slice(row.mark.length)}` : line
|
|
871
|
+
|
|
872
|
+
/** The text from `from` on dimmed, without the blanks that pad it to the right edge. */
|
|
873
|
+
const dimmedRest = (text: string, from: number): string => {
|
|
874
|
+
const end = text.trimEnd().length
|
|
875
|
+
return from >= end ? text : `${text.slice(0, from)}${paint.dim(text.slice(from, end))}${text.slice(end)}`
|
|
876
|
+
}
|
|
794
877
|
|
|
795
878
|
/**
|
|
796
|
-
*
|
|
797
|
-
* and
|
|
798
|
-
*
|
|
879
|
+
* One row as lines: the bar on each, the mark painted on the first line only, the short title in the normal colour
|
|
880
|
+
* and the rest of an expanded row dim, linked when the row has an address; the `[ask]` button at the end of a
|
|
881
|
+
* question's first line wears the mark's paint.
|
|
799
882
|
*/
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
883
|
+
const blockRowLines = ({ bar, markPaint }: TopBlock, row: BlockRow): string[] =>
|
|
884
|
+
row.lines.map((line, index) => {
|
|
885
|
+
const withButton = index === 0 && row.askColumn !== null
|
|
886
|
+
const styled = dimmedRest(line, row.dimFrom[index] ?? line.length)
|
|
887
|
+
const marked = index === 0 ? markedLine(styled, row, markPaint) : styled
|
|
888
|
+
const text = withButton ? `${marked}${markPaint(ASK_BUTTON)}` : marked
|
|
889
|
+
return `${bar()} ${row.url ? link(row.url, text) : text}`
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
/** A folded block: the one line `▸ <name> · <n>` and the first title as a preview; a click matches no row and no `+<n>` line. */
|
|
893
|
+
const foldedBlockLines = ({ name, keys, rows }: TopBlock, width: number): BlockLines => {
|
|
894
|
+
const preview = rows[0]?.preview
|
|
895
|
+
const head = `${chevronOf(false)} ${name} ${ITEM_SEPARATOR} ${keys.length}`
|
|
896
|
+
return {
|
|
897
|
+
open: false,
|
|
898
|
+
lines: [paint.dim(fit(preview === undefined ? head : `${head} ${preview}`, width))],
|
|
899
|
+
hit: { rows: [], moreKey: null, keys },
|
|
900
|
+
}
|
|
806
901
|
}
|
|
807
902
|
|
|
808
|
-
/**
|
|
809
|
-
|
|
810
|
-
|
|
903
|
+
/**
|
|
904
|
+
* An open block on top of the herd: the head `▾ <name> · <n> ──`, one barred line per row (the first five, all while the
|
|
905
|
+
* block is expanded) and `▌ +<n>` for the rest, which turns into `▌ ▴ <n>` once all show and folds them again. A row wider than the pane is cut to
|
|
906
|
+
* one line; once expanded it wraps over several. The rule below is the stack's, not the block's.
|
|
907
|
+
*/
|
|
908
|
+
const openBlockLines = (block: TopBlock, width: number): BlockLines => {
|
|
909
|
+
const count = block.keys.length
|
|
910
|
+
const more = Math.max(count - SHOWN_BLOCK_ROWS, 0)
|
|
911
|
+
const moreLine = block.expanded ? `${LESS_MARK} ${more}` : `+${more}`
|
|
912
|
+
return {
|
|
913
|
+
open: true,
|
|
914
|
+
lines: [
|
|
915
|
+
blockHead(block.name, count, width, count > 0 ? block.countPaint : (text) => text),
|
|
916
|
+
...block.rows.flatMap((row) => blockRowLines(block, row)),
|
|
917
|
+
...(more > 0 ? [`${block.bar()} ${moreLine}`] : []),
|
|
918
|
+
],
|
|
919
|
+
hit: { rows: block.rows, moreKey: moreKeyOf(count, block.moreKey), keys: block.keys },
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const blockLinesOf = (block: TopBlock, state: Selection, id: TopBlockId, width: number): BlockLines =>
|
|
924
|
+
isBlockFolded(state, id, block.keys) ? foldedBlockLines(block, width) : openBlockLines(block, width)
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* What waits on the user's answer, on top of the herd until it is closed: `▌ 📎 name` for a sent file or an
|
|
928
|
+
* Artifact, `▌ ? question` for a question of the board or of a session's dialog, in the amber of a lane that waits.
|
|
929
|
+
* One line while the user folded it.
|
|
930
|
+
*/
|
|
931
|
+
const waitingLines = (waiting: readonly WaitingItem[], state: Selection, width: number): BlockLines => {
|
|
932
|
+
const keys = waitingKeys(waiting)
|
|
933
|
+
const block: TopBlock = {
|
|
934
|
+
name: TEXTS.waitingHeading,
|
|
935
|
+
bar: waitingBar,
|
|
936
|
+
markPaint: STATUS_PAINT[AGENT_STATUS.blocked],
|
|
937
|
+
countPaint: STATUS_PAINT[AGENT_STATUS.blocked],
|
|
938
|
+
moreKey: WAITING_MORE_KEY,
|
|
939
|
+
expanded: state.expanded.has(WAITING_MORE_KEY),
|
|
940
|
+
keys,
|
|
941
|
+
rows: waitingRows(waiting, state.expanded, width),
|
|
942
|
+
}
|
|
943
|
+
return blockLinesOf(block, state, TOP_BLOCK.waiting, width)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** The open appointments of the board under the for-you block: `▌ ◷ Mo 09:03 Release 2.0 rollout`, in a grey bar and with a grey mark. */
|
|
947
|
+
const appointmentLines = ({ rows }: AppointmentBlock, state: Selection, width: number): BlockLines => {
|
|
948
|
+
const keys = appointmentKeys(rows)
|
|
949
|
+
const block: TopBlock = {
|
|
950
|
+
name: TEXTS.appointmentHeading,
|
|
951
|
+
bar: appointmentBar,
|
|
952
|
+
markPaint: paint.dim,
|
|
953
|
+
countPaint: (text) => text,
|
|
954
|
+
moreKey: APPOINTMENT_MORE_KEY,
|
|
955
|
+
expanded: state.expanded.has(APPOINTMENT_MORE_KEY),
|
|
956
|
+
keys,
|
|
957
|
+
rows: blockRows(appointmentItems(rows), state.expanded, width, APPOINTMENT_MORE_KEY),
|
|
958
|
+
}
|
|
959
|
+
return blockLinesOf(block, state, TOP_BLOCK.appointments, width)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const appointmentLinesOf = (
|
|
963
|
+
appointments: AppointmentBlock | undefined,
|
|
964
|
+
state: Selection,
|
|
965
|
+
width: number,
|
|
966
|
+
): BlockLines | null =>
|
|
967
|
+
appointments && appointments.rows.length > 0 ? appointmentLines(appointments, state, width) : null
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* The blocks one under the other, `offset` lines below the top of the screen, with one rule between neighbours: the
|
|
971
|
+
* head of an open block is that rule, a folded block gets a plain one above it unless a folded block stands right
|
|
972
|
+
* above (folded blocks share the rule below them), and a plain rule closes the stack. An
|
|
973
|
+
* absent block (null) takes no line and has no hit.
|
|
974
|
+
*/
|
|
975
|
+
const stackBlocks = (slots: readonly (BlockLines | null)[], offset: number, width: number): BlockStack => {
|
|
976
|
+
const lines: string[] = []
|
|
977
|
+
const hits: (BlockHit | null)[] = []
|
|
978
|
+
let previousFolded = false
|
|
979
|
+
for (const slot of slots) {
|
|
980
|
+
if (slot && !slot.open && !previousFolded) lines.push(rule(width))
|
|
981
|
+
hits.push(slot && { ...slot.hit, firstLine: offset + lines.length + 1 })
|
|
982
|
+
if (slot) {
|
|
983
|
+
lines.push(...slot.lines)
|
|
984
|
+
previousFolded = !slot.open
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return { lines: lines.length > 0 ? [...lines, rule(width)] : lines, hits }
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* A dimmed rule before every section of the body but the first, which shows where one ends; none between two
|
|
992
|
+
* one-line lane blocks (the first `laneCount` sections), so folded lanes stand together as a list.
|
|
993
|
+
*/
|
|
994
|
+
const separate = (sections: Frame[], width: number, laneCount: number): Frame[] =>
|
|
995
|
+
sections.flatMap((frame, index) => {
|
|
996
|
+
if (index === 0) return [frame]
|
|
997
|
+
const listed = index < laneCount && frame.lines.length === 1 && sections[index - 1]?.lines.length === 1
|
|
998
|
+
return listed ? [frame] : [textFrame([rule(width)]), frame]
|
|
999
|
+
})
|
|
811
1000
|
|
|
812
1001
|
/**
|
|
813
1002
|
* Why badges may be missing, said beside the clock: no watcher runs, or its state is stale. Nothing while it is live,
|
|
@@ -856,11 +1045,11 @@ const partitionLanes = (lanes: LaneView[]): Record<'active' | 'waiting' | 'noAge
|
|
|
856
1045
|
/**
|
|
857
1046
|
* Title and clock on top, the key hint at the bottom (with the legend after `?`), and in between one
|
|
858
1047
|
* block per lane: the lane line with its total bar, and for an open lane the phase rows. Folded lanes are
|
|
859
|
-
* one line each and
|
|
1048
|
+
* one line each, and a dimmed rule stands between lane blocks unless both are one line. Lanes that wait and
|
|
860
1049
|
* lanes with a worktree but no agent each share a closing block; finished lanes are not drawn.
|
|
861
1050
|
*/
|
|
862
1051
|
export const renderScreen = (
|
|
863
|
-
{ title, time, lanes, planned, watch, watchesTickets, awaiting = [], unplaced = [] }: OverviewView,
|
|
1052
|
+
{ title, time, lanes, planned, watch, watchesTickets, awaiting = [], appointments, unplaced = [] }: OverviewView,
|
|
864
1053
|
state: Selection,
|
|
865
1054
|
width: number,
|
|
866
1055
|
): Screen => {
|
|
@@ -871,19 +1060,24 @@ export const renderScreen = (
|
|
|
871
1060
|
const blocks = active.map((lane) => laneBlock(lane, state, width, columns, badgeTexts))
|
|
872
1061
|
const notice = watchNotice({ lanes: active, watch, watchesTickets }, badgeTexts)
|
|
873
1062
|
const clock = notice ? `${paint.yellow(notice)}${COLUMN_SEPARATOR}${paint.dim(time)}` : paint.dim(time)
|
|
874
|
-
const
|
|
1063
|
+
const titleLine = spread(`${HEADER_MARK} ${paint.bold(title)}`, clock, width)
|
|
1064
|
+
const forYou = awaiting.length > 0 ? waitingLines(awaiting, state, width) : null
|
|
1065
|
+
const dated = appointmentLinesOf(appointments, state, width)
|
|
1066
|
+
const nowLines = unplaced.map((task) => nowLine(task, width))
|
|
1067
|
+
const above = nowLines.length > 0 ? [titleLine, rule(width), ...nowLines] : [titleLine]
|
|
1068
|
+
const stack = stackBlocks([forYou, dated], above.length, width)
|
|
1069
|
+
const endsWithRule = stack.lines.length > 0 || nowLines.length === 0
|
|
1070
|
+
const sections = [
|
|
1071
|
+
...blocks,
|
|
1072
|
+
...(parked.length > 0 ? [textFrame(parked)] : []),
|
|
1073
|
+
...(planned.length > 0 ? [plannedFrame(planned, state, width)] : []),
|
|
1074
|
+
]
|
|
1075
|
+
const lead = !endsWithRule && blocks.length === 0 && sections.length > 0 ? [textFrame([rule(width)])] : []
|
|
875
1076
|
return {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
...(awaiting.length > 0 ? [...waitingLines(awaiting, state.expanded, width), barkingRule(width)] : []),
|
|
881
|
-
],
|
|
882
|
-
body: joinFrames([
|
|
883
|
-
...separate(blocks, width),
|
|
884
|
-
...(parked.length > 0 ? [textFrame([rule(width), ...parked])] : []),
|
|
885
|
-
...(planned.length > 0 ? [textFrame([rule(width)]), plannedFrame(planned, state, width)] : []),
|
|
886
|
-
]),
|
|
1077
|
+
waitingBlock: stack.hits[0],
|
|
1078
|
+
appointmentBlock: stack.hits[1],
|
|
1079
|
+
header: [...above, ...(stack.lines.length > 0 || nowLines.length > 0 ? stack.lines : [rule(width)])],
|
|
1080
|
+
body: joinFrames([...lead, ...separate(sections, width, blocks.length)]),
|
|
887
1081
|
footer: [
|
|
888
1082
|
rule(width),
|
|
889
1083
|
fit(paint.dim(TEXTS.hint), width),
|
package/src/plans/link-chips.ts
CHANGED
|
@@ -14,6 +14,7 @@ const MARKS = /\*\*|`/g
|
|
|
14
14
|
const CONTROL = /\p{Cc}/gu
|
|
15
15
|
|
|
16
16
|
const linksOf = (line: string): Found[] => {
|
|
17
|
+
if (!line.includes('http')) return []
|
|
17
18
|
const found: Found[] = []
|
|
18
19
|
const take = (url: string, text: string | null, index: number) =>
|
|
19
20
|
found.push({ url, text, before: line.slice(0, index), line })
|