@marcomigozzi/trailhead 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.it.md +118 -30
- package/README.md +107 -29
- package/bin/base-preflight-docs.test.js +65 -0
- package/bin/execute-split-docs.test.js +146 -0
- package/bin/isolation-answer-docs.test.js +41 -0
- package/bin/isolation-copy-docs.test.js +28 -0
- package/bin/lib/codex-projection.js +1 -1
- package/bin/lib/codex-projection.test.js +41 -0
- package/bin/report-status-docs.test.js +195 -0
- package/bin/resolve-helpers-docs.test.js +36 -0
- package/bin/scope-confirm-skip-docs.test.js +70 -0
- package/bin/trailhead.js +3 -3
- package/bin/trailhead.test.js +341 -1
- package/bin/uat-setup-docs.test.js +65 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
- package/plugins/trailhead/agents/trailhead-debug.md +20 -1
- package/plugins/trailhead/agents/trailhead-executor.md +29 -2
- package/plugins/trailhead/agents/trailhead-fix.md +29 -2
- package/plugins/trailhead/agents/trailhead-plan.md +20 -1
- package/plugins/trailhead/agents/trailhead-research.md +22 -3
- package/plugins/trailhead/agents/trailhead-verify.md +15 -2
- package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
- package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
- package/plugins/trailhead/hooks/lib/dashboard.test.js +409 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
- package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
- package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
- package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
- package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
- package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
- package/plugins/trailhead/hooks/mod/README.md +38 -6
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +168 -15
- package/plugins/trailhead/hooks/mod/map-pane.tsx +275 -106
- package/plugins/trailhead/hooks/mod/register.tsx +15 -2
- package/plugins/trailhead/hooks/mod/work-state-view.ts +17 -0
- package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
- package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
- package/plugins/trailhead/skills/_shared/choices.md +4 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +15 -3
- package/plugins/trailhead/skills/_shared/load-first.md +3 -3
- package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +22 -11
- package/plugins/trailhead/skills/_shared/substrate.md +6 -4
- package/plugins/trailhead/skills/_shared/teamwork.md +16 -7
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +17 -9
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +12 -2
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +8 -6
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +14 -12
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +4 -4
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +26 -10
- package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
- package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
- package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +340 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
- package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
- package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
- package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
- package/plugins/trailhead/tests/map-pane-ui.test.ts +566 -19
- package/plugins/trailhead/tests/map-pane-view.test.ts +304 -16
- package/plugins/trailhead/tests/work-state-view.test.ts +25 -1
- package/plugins/trailhead/types/index.d.ts +37 -3
|
@@ -5,22 +5,31 @@ import {
|
|
|
5
5
|
axisStart,
|
|
6
6
|
burndownEnd,
|
|
7
7
|
burndownSeries,
|
|
8
|
+
countsLabel,
|
|
8
9
|
failureReason,
|
|
9
10
|
footerStatus,
|
|
10
11
|
groupTickets,
|
|
12
|
+
headingRoom,
|
|
11
13
|
HISTORY_LIMIT,
|
|
12
14
|
mapRowLayout,
|
|
13
15
|
nextViewed,
|
|
14
16
|
parseActiveMap,
|
|
17
|
+
parseDashboard,
|
|
15
18
|
parseHistory,
|
|
16
19
|
parseMap,
|
|
17
20
|
parseMaps,
|
|
21
|
+
parseMapsRead,
|
|
18
22
|
parseTickets,
|
|
23
|
+
parseUatParents,
|
|
19
24
|
rasterCells,
|
|
20
25
|
refreshDecision,
|
|
21
26
|
rowLayout,
|
|
22
27
|
sparkline,
|
|
28
|
+
uatParentLine,
|
|
23
29
|
viewTarget,
|
|
30
|
+
withUatParents,
|
|
31
|
+
whiteboardHeading,
|
|
32
|
+
whiteboardRowLayout,
|
|
24
33
|
} from '../hooks/mod/map-pane-view'
|
|
25
34
|
|
|
26
35
|
const issue = (over: Record<string, unknown> = {}) => ({
|
|
@@ -57,10 +66,10 @@ test('parseTickets maps a gh list to rows with type, state and holders', () => {
|
|
|
57
66
|
)
|
|
58
67
|
expect(out?.unverifiedCount).toBe(0)
|
|
59
68
|
expect(out?.tickets).toEqual([
|
|
60
|
-
{ number: 209, title: 'Burn-down view', type: 'build', state: 'frontier', assignees: [], url: 'https://github.com/o/r/issues/209' },
|
|
61
|
-
{ number: 212, title: 'Held', type: 'research', state: 'in-progress', assignees: ['alice'], url: 'https://github.com/o/r/issues/209' },
|
|
62
|
-
{ number: 211, title: 'Gated', type: 'bug', state: 'blocked', assignees: ['bob'], url: 'https://github.com/o/r/issues/209' },
|
|
63
|
-
{ number: 214, title: 'No type', type: null, state: 'frontier', assignees: [], url: 'https://github.com/o/r/issues/209' },
|
|
69
|
+
{ number: 209, title: 'Burn-down view', type: 'build', state: 'frontier', assignees: [], url: 'https://github.com/o/r/issues/209', isUat: false, uatOf: null },
|
|
70
|
+
{ number: 212, title: 'Held', type: 'research', state: 'in-progress', assignees: ['alice'], url: 'https://github.com/o/r/issues/209', isUat: false, uatOf: null },
|
|
71
|
+
{ number: 211, title: 'Gated', type: 'bug', state: 'blocked', assignees: ['bob'], url: 'https://github.com/o/r/issues/209', isUat: false, uatOf: null },
|
|
72
|
+
{ number: 214, title: 'No type', type: null, state: 'frontier', assignees: [], url: 'https://github.com/o/r/issues/209', isUat: false, uatOf: null },
|
|
64
73
|
])
|
|
65
74
|
})
|
|
66
75
|
|
|
@@ -140,13 +149,102 @@ test('parseMap has a null destination when the section is missing or empty, and
|
|
|
140
149
|
})
|
|
141
150
|
|
|
142
151
|
test('groupTickets splits by state, ascending by number', () => {
|
|
143
|
-
const t = (number: number, state: 'frontier' | 'in-progress' | 'blocked') => ({
|
|
152
|
+
const t = (number: number, state: 'frontier' | 'in-progress' | 'blocked' | 'paused') => ({
|
|
144
153
|
number, title: '', type: null, state, assignees: [], url: '',
|
|
145
154
|
})
|
|
146
155
|
const g = groupTickets([t(30, 'frontier'), t(5, 'blocked'), t(9, 'in-progress'), t(12, 'frontier'), t(2, 'blocked')])
|
|
147
156
|
expect(g.frontier.map(x => x.number)).toEqual([12, 30])
|
|
148
157
|
expect(g.inProgress.map(x => x.number)).toEqual([9])
|
|
149
158
|
expect(g.blocked.map(x => x.number)).toEqual([2, 5])
|
|
159
|
+
expect(g.paused).toEqual([])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('groupTickets puts a paused ticket in the Paused group only', () => {
|
|
163
|
+
const t = (number: number, state: 'frontier' | 'in-progress' | 'blocked' | 'paused') => ({
|
|
164
|
+
number, title: '', type: null, state, assignees: [], url: '',
|
|
165
|
+
})
|
|
166
|
+
const g = groupTickets([t(8, 'paused'), t(3, 'paused'), t(4, 'in-progress'), t(5, 'blocked'), t(6, 'frontier')])
|
|
167
|
+
expect(g.paused.map(x => x.number)).toEqual([3, 8])
|
|
168
|
+
expect([g.frontier, g.inProgress, g.blocked].map(x => x.map(y => y.number))).toEqual([[6], [4], [5]])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('parseTickets: a paused label wins over blocked and in-progress, with the holder kept', () => {
|
|
172
|
+
const state = (names: string[], assignees: string[] = []) =>
|
|
173
|
+
parseTickets(list(issue({ labels: labels(...names), assignees: assignees.map(login => ({ login })) })))?.tickets[0]
|
|
174
|
+
expect(state(['trailhead:build', 'trailhead:paused'])).toMatchObject({ state: 'paused', assignees: [] })
|
|
175
|
+
expect(state(['trailhead:build', 'trailhead:paused', 'trailhead:blocked'], ['alice'])).toMatchObject({ state: 'paused', assignees: ['alice'] })
|
|
176
|
+
expect(state(['trailhead:build', 'trailhead:paused'], ['alice'])?.state).toBe('paused')
|
|
177
|
+
expect(state(['trailhead:build'], ['alice'])?.state).toBe('in-progress')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('parseTickets sets isUat from the trailhead:uat label, with no parent yet', () => {
|
|
181
|
+
const t = (...names: string[]) => parseTickets(list(issue({ labels: labels(...names) })))?.tickets[0]
|
|
182
|
+
expect(t('trailhead:task', 'trailhead:uat')).toMatchObject({ isUat: true, uatOf: null, state: 'frontier' })
|
|
183
|
+
expect(t('trailhead:task')).toMatchObject({ isUat: false, uatOf: null })
|
|
184
|
+
// a marker, not a state: the state is unchanged
|
|
185
|
+
expect(t('trailhead:seed', 'trailhead:blocked', 'trailhead:uat')).toMatchObject({ isUat: true, state: 'blocked' })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const body = (text: string, number = 5) => ({ number, body: text })
|
|
189
|
+
const uatList = (...rows: unknown[]) => JSON.stringify(rows)
|
|
190
|
+
|
|
191
|
+
test('parseUatParents maps a ticket to the parent named by its UAT of: line', () => {
|
|
192
|
+
const out = parseUatParents(
|
|
193
|
+
uatList(
|
|
194
|
+
body('Intro\n\nUAT of: [Burn-down view](https://github.com/o/r/issues/209)\n\n- [ ] step', 301),
|
|
195
|
+
body('UAT of: #210', 302),
|
|
196
|
+
body('no pointer here', 303),
|
|
197
|
+
body('mentions UAT of: [x](https://github.com/o/r/issues/1) mid-line', 304),
|
|
198
|
+
body('UAT of: [Win](https://github.com/o/r/issues/212)\r\nmore', 305),
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
expect(out?.get(301)).toEqual({ number: 209, title: 'Burn-down view' })
|
|
202
|
+
expect(out?.get(302)).toEqual({ number: 210, title: '' })
|
|
203
|
+
expect(out?.get(303)).toBeUndefined()
|
|
204
|
+
expect(out?.get(304)).toBeUndefined()
|
|
205
|
+
expect(out?.get(305)).toEqual({ number: 212, title: 'Win' })
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('parseUatParents strips control characters from a parent title and skips unusable rows', () => {
|
|
209
|
+
const out = parseUatParents(
|
|
210
|
+
uatList(body('UAT of: [a\u0007bcd](https://github.com/o/r/issues/7)', 1), { number: 'x', body: 'UAT of: #3' }, { number: 2, body: 5 }, 'junk', body('UAT of: [t](https://example.com/nope)', 3)),
|
|
211
|
+
)
|
|
212
|
+
expect(out?.get(1)).toEqual({ number: 7, title: 'abcd' })
|
|
213
|
+
expect(out?.size).toBe(1)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('parseUatParents is null on non-JSON or a non-list', () => {
|
|
217
|
+
expect(parseUatParents('{nope')).toBeNull()
|
|
218
|
+
expect(parseUatParents('{}')).toBeNull()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('withUatParents sets the parent on a UAT ticket only, and leaves the tickets alone without a read', () => {
|
|
222
|
+
const t = (number: number, isUat: boolean) => ({ number, title: '', type: null, state: 'frontier' as const, assignees: [], url: '', isUat, uatOf: null })
|
|
223
|
+
const tickets = [t(1, true), t(2, false), t(3, true)]
|
|
224
|
+
const out = withUatParents(tickets, new Map([[1, { number: 9, title: 'P' }], [2, { number: 8, title: 'Q' }]]))
|
|
225
|
+
expect(out.map(x => x.uatOf)).toEqual([{ number: 9, title: 'P' }, null, null])
|
|
226
|
+
expect(tickets[0]?.uatOf).toBeNull()
|
|
227
|
+
expect(withUatParents(tickets, null)).toEqual(tickets)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('uatParentLine draws tests <title> #<n>, fitted to the width, and nothing without a parent', () => {
|
|
231
|
+
const t = (uatOf: { number: number; title: string } | null) => ({ number: 1, title: '', type: null, state: 'frontier' as const, assignees: [], url: '', isUat: true, uatOf })
|
|
232
|
+
expect(uatParentLine(t(null), 60)).toBeNull()
|
|
233
|
+
expect(uatParentLine(t({ number: 209, title: 'Burn-down view' }), 60)).toBe('tests Burn-down view #209')
|
|
234
|
+
expect(uatParentLine(t({ number: 210, title: '' }), 60)).toBe('tests #210')
|
|
235
|
+
const cut = uatParentLine(t({ number: 209, title: 'x'.repeat(200) }), 30) as string
|
|
236
|
+
expect(Array.from(cut).length).toBe(30)
|
|
237
|
+
expect(cut.endsWith('… #209')).toBe(true)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('groupTickets lists a UAT ticket once, under UAT, ascending; a paused one stays under Paused', () => {
|
|
241
|
+
const t = (number: number, state: 'frontier' | 'in-progress' | 'blocked' | 'paused', isUat: boolean) => ({
|
|
242
|
+
number, title: '', type: null, state, assignees: [], url: '', isUat, uatOf: null,
|
|
243
|
+
})
|
|
244
|
+
const g = groupTickets([t(9, 'frontier', true), t(4, 'blocked', true), t(6, 'in-progress', true), t(7, 'paused', true), t(2, 'frontier', false), t(3, 'blocked', false)])
|
|
245
|
+
expect(g.uat.map(x => x.number)).toEqual([4, 6, 9])
|
|
246
|
+
expect(g.paused.map(x => x.number)).toEqual([7])
|
|
247
|
+
expect([g.frontier, g.inProgress, g.blocked].map(x => x.map(y => y.number))).toEqual([[2], [], [3]])
|
|
150
248
|
})
|
|
151
249
|
|
|
152
250
|
test('failureReason names one short cause', () => {
|
|
@@ -223,6 +321,28 @@ test('rowLayout: holders are drawn as @login on in-progress and blocked rows', (
|
|
|
223
321
|
expect(loose.isDim).toBe(true)
|
|
224
322
|
})
|
|
225
323
|
|
|
324
|
+
test('rowLayout: a paused row shows its holder when assigned, never a work button', () => {
|
|
325
|
+
expect(layout(ticket({ state: 'paused', assignees: ['alice'] })).aside).toEqual({ kind: 'holder', text: '@alice' })
|
|
326
|
+
expect(layout(ticket({ state: 'paused' })).aside).toBeNull()
|
|
327
|
+
expect(layout(ticket({ state: 'paused' }), { pendingWork: 209 }).aside).toBeNull()
|
|
328
|
+
expect(layout(ticket({ state: 'paused' })).isDim).toBe(false)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('rowLayout: the ticket in play says this session instead of its holder', () => {
|
|
332
|
+
const r = layout(ticket({ state: 'in-progress', assignees: ['me'] }), { isInPlay: true, hasMarkColumn: true })
|
|
333
|
+
expect(r.aside).toEqual({ kind: 'self', text: 'this session' })
|
|
334
|
+
expect(layout(ticket({ state: 'in-progress', assignees: ['me'] }), { hasMarkColumn: true }).aside).toEqual({ kind: 'holder', text: '@me' })
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test('rowLayout: the this-session aside is counted in the row width', () => {
|
|
338
|
+
const t = ticket({ title: 'x'.repeat(200), state: 'in-progress', assignees: ['me'] })
|
|
339
|
+
for (const columns of [30, 40, 60]) {
|
|
340
|
+
const r = layout(t, { columns, isInPlay: true, hasMarkColumn: true })
|
|
341
|
+
const used = (r.mark?.length ?? 0) + r.label.length + (r.type?.length ?? 0) + (r.aside?.text.length ?? 0) + 1
|
|
342
|
+
expect(used).toBeLessThanOrEqual(columns)
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
226
346
|
test('rowLayout: the mark column marks the ticket in play and pads the others', () => {
|
|
227
347
|
expect(layout(ticket({ state: 'in-progress', assignees: ['me'] }), { isInPlay: true, hasMarkColumn: true })).toMatchObject({ mark: '▸ ', isInPlay: true })
|
|
228
348
|
expect(layout(ticket(), { hasMarkColumn: true }).mark).toBe(' ')
|
|
@@ -473,9 +593,9 @@ test('parseMaps keeps the open maps sorted by number, titles flattened', () => {
|
|
|
473
593
|
]),
|
|
474
594
|
)
|
|
475
595
|
expect(out).toEqual([
|
|
476
|
-
{ number: 14, title: 'older map' },
|
|
477
|
-
{ number: 20, title: '' },
|
|
478
|
-
{ number: 198, title: 'trailhead mod' },
|
|
596
|
+
{ number: 14, title: 'older map', counts: null },
|
|
597
|
+
{ number: 20, title: '', counts: null },
|
|
598
|
+
{ number: 198, title: 'trailhead mod', counts: null },
|
|
479
599
|
])
|
|
480
600
|
expect(parseMaps('[]')).toEqual([])
|
|
481
601
|
})
|
|
@@ -486,6 +606,135 @@ test('parseMaps rejects what is not a list', () => {
|
|
|
486
606
|
expect(parseMaps('null')).toBeNull()
|
|
487
607
|
})
|
|
488
608
|
|
|
609
|
+
const WB_FIELDS = { whiteboardOpen: { totalCount: 3 }, whiteboardAll: { totalCount: 9 } }
|
|
610
|
+
const gql = (nodes: unknown[], extra: Record<string, unknown> = {}, repo: Record<string, unknown> = WB_FIELDS) =>
|
|
611
|
+
JSON.stringify({ data: { repository: { issues: { nodes }, ...repo } }, ...extra })
|
|
612
|
+
|
|
613
|
+
test('parseMapsRead reads each map with its open and total sub-issues, sorted by number', () => {
|
|
614
|
+
const out = parseMapsRead(
|
|
615
|
+
gql([
|
|
616
|
+
{ number: 198, title: 'trailhead\u202e mod\n', url: 'https://x/198', subIssuesSummary: { total: 23, completed: 16 } },
|
|
617
|
+
{ number: 14, title: 'older map', url: 'https://x/14', subIssuesSummary: { total: 5, completed: 5 } },
|
|
618
|
+
{ number: 0, title: 'bad number', subIssuesSummary: { total: 1, completed: 0 } },
|
|
619
|
+
'junk',
|
|
620
|
+
]),
|
|
621
|
+
)
|
|
622
|
+
expect(out?.maps).toEqual([
|
|
623
|
+
{ number: 14, title: 'older map', counts: { open: 0, total: 5 } },
|
|
624
|
+
{ number: 198, title: 'trailhead mod', counts: { open: 7, total: 23 } },
|
|
625
|
+
])
|
|
626
|
+
expect(parseMapsRead(gql([]))?.maps).toEqual([])
|
|
627
|
+
// completed above total never goes negative
|
|
628
|
+
expect(parseMapsRead(gql([{ number: 3, title: 't', subIssuesSummary: { total: 2, completed: 4 } }]))?.maps).toEqual([
|
|
629
|
+
{ number: 3, title: 't', counts: { open: 0, total: 2 } },
|
|
630
|
+
])
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
test('parseMapsRead keeps a row with a malformed summary, name-only', () => {
|
|
634
|
+
const out = parseMapsRead(
|
|
635
|
+
gql([
|
|
636
|
+
{ number: 1, title: 'a' },
|
|
637
|
+
{ number: 2, title: 'b', subIssuesSummary: { total: 'x', completed: 1 } },
|
|
638
|
+
{ number: 3, title: 'c', subIssuesSummary: null },
|
|
639
|
+
]),
|
|
640
|
+
)
|
|
641
|
+
expect(out?.maps).toEqual([
|
|
642
|
+
{ number: 1, title: 'a', counts: null },
|
|
643
|
+
{ number: 2, title: 'b', counts: null },
|
|
644
|
+
{ number: 3, title: 'c', counts: null },
|
|
645
|
+
])
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
test('parseMapsRead reads the whiteboard counts beside the maps', () => {
|
|
649
|
+
expect(parseMapsRead(gql([]))?.whiteboard).toEqual({ open: 3, total: 9 })
|
|
650
|
+
// nothing on the whiteboard is a real count, drawn as is
|
|
651
|
+
const none = { whiteboardOpen: { totalCount: 0 }, whiteboardAll: { totalCount: 0 } }
|
|
652
|
+
expect(parseMapsRead(gql([], {}, none))?.whiteboard).toEqual({ open: 0, total: 0 })
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
test('parseMapsRead gives a null whiteboard, maps intact, when its fields are missing or unusable', () => {
|
|
656
|
+
const node = { number: 1, title: 'a', subIssuesSummary: { total: 4, completed: 1 } }
|
|
657
|
+
const bad: Record<string, unknown>[] = [
|
|
658
|
+
{},
|
|
659
|
+
{ whiteboardOpen: { totalCount: 3 } },
|
|
660
|
+
{ whiteboardAll: { totalCount: 9 } },
|
|
661
|
+
{ whiteboardOpen: null, whiteboardAll: { totalCount: 9 } },
|
|
662
|
+
{ whiteboardOpen: { totalCount: -1 }, whiteboardAll: { totalCount: 9 } },
|
|
663
|
+
{ whiteboardOpen: { totalCount: 1.5 }, whiteboardAll: { totalCount: 9 } },
|
|
664
|
+
{ whiteboardOpen: { totalCount: '3' }, whiteboardAll: { totalCount: 9 } },
|
|
665
|
+
{ whiteboardOpen: { totalCount: 10 }, whiteboardAll: { totalCount: 9 } },
|
|
666
|
+
]
|
|
667
|
+
for (const repo of bad) {
|
|
668
|
+
expect(parseMapsRead(gql([node], {}, repo))).toEqual({ maps: [{ number: 1, title: 'a', counts: { open: 3, total: 4 } }], whiteboard: null })
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
test('parseMapsRead is null on anything but a clean reply', () => {
|
|
673
|
+
const node = { number: 1, title: 'a', subIssuesSummary: { total: 1, completed: 0 } }
|
|
674
|
+
expect(parseMapsRead('not json')).toBeNull()
|
|
675
|
+
expect(parseMapsRead('[]')).toBeNull()
|
|
676
|
+
expect(parseMapsRead('{}')).toBeNull()
|
|
677
|
+
expect(parseMapsRead('{"data":null}')).toBeNull()
|
|
678
|
+
expect(parseMapsRead('{"data":{"repository":null}}')).toBeNull()
|
|
679
|
+
expect(parseMapsRead('{"data":{"repository":{"issues":{"nodes":"x"}}}}')).toBeNull()
|
|
680
|
+
expect(parseMapsRead(JSON.stringify({ errors: [{ message: 'boom' }] }))).toBeNull()
|
|
681
|
+
// partial data next to errors is still a failed read
|
|
682
|
+
expect(parseMapsRead(gql([node], { errors: [{ message: 'partial' }] }))).toBeNull()
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
test('countsLabel words open over total', () => {
|
|
686
|
+
expect(countsLabel({ open: 7, total: 23 })).toBe('7 open / 23')
|
|
687
|
+
expect(countsLabel({ open: 0, total: 0 })).toBe('0 open / 0')
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
test('whiteboardRowLayout puts the counts beside the name, a null read leaves it name-only', () => {
|
|
691
|
+
expect(whiteboardRowLayout({ open: 3, total: 9 }, 60)).toEqual({ mark: ' ', label: 'Whiteboard', aside: ' 3 open / 9' })
|
|
692
|
+
expect(whiteboardRowLayout({ open: 0, total: 0 }, 60).aside).toBe(' 0 open / 0')
|
|
693
|
+
expect(whiteboardRowLayout(null, 60)).toEqual({ mark: ' ', label: 'Whiteboard', aside: null })
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
test('whiteboardRowLayout keeps the name whole and drops the counts when they do not fit', () => {
|
|
697
|
+
// mark 2 + `[ Whiteboard ]` 14 + ` 3 open / 9` 11 = 27 cells
|
|
698
|
+
expect(whiteboardRowLayout({ open: 3, total: 9 }, 27).aside).toBe(' 3 open / 9')
|
|
699
|
+
const narrow = whiteboardRowLayout({ open: 3, total: 9 }, 26)
|
|
700
|
+
expect(narrow.aside).toBeNull()
|
|
701
|
+
expect(narrow.label).toBe('Whiteboard')
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
test('mapRowLayout asides: counts first, then active, joined by a dot', () => {
|
|
705
|
+
const m = { number: 198, title: 'trailhead mod', counts: { open: 7, total: 23 } }
|
|
706
|
+
expect(mapRowLayout(m, { active: 198, viewed: null, columns: 60 }).aside).toBe(' 7 open / 23 · active')
|
|
707
|
+
expect(mapRowLayout(m, { active: 14, viewed: null, columns: 60 }).aside).toBe(' 7 open / 23')
|
|
708
|
+
expect(mapRowLayout({ ...m, counts: null }, { active: 198, viewed: null, columns: 60 }).aside).toBe(' active')
|
|
709
|
+
expect(mapRowLayout({ ...m, counts: null }, { active: 14, viewed: null, columns: 60 }).aside).toBeNull()
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test('mapRowLayout cuts the title before the counts, which stay whole', () => {
|
|
713
|
+
const m = { number: 14, title: 'x'.repeat(200), counts: { open: 7, total: 23 } }
|
|
714
|
+
const r = mapRowLayout(m, { active: 198, viewed: null, columns: 40 })
|
|
715
|
+
expect(r.aside).toBe(' 7 open / 23')
|
|
716
|
+
expect(r.label.endsWith('…')).toBe(true)
|
|
717
|
+
expect(r.mark.length + r.label.length + 4 + (r.aside?.length ?? 0)).toBeLessThanOrEqual(40)
|
|
718
|
+
// a map with no title falls back to the number
|
|
719
|
+
expect(mapRowLayout({ ...m, title: '' }, { active: 198, viewed: null, columns: 40 }).label).toBe('#14')
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
const wbTicket = (number: number, assignees: string[] = []) => ({
|
|
723
|
+
number,
|
|
724
|
+
title: `t${number}`,
|
|
725
|
+
type: null,
|
|
726
|
+
state: assignees.length > 0 ? ('in-progress' as const) : ('frontier' as const),
|
|
727
|
+
assignees,
|
|
728
|
+
url: '',
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
test('whiteboardHeading counts the open and the assigned tickets', () => {
|
|
732
|
+
expect(whiteboardHeading([wbTicket(1), wbTicket(2, ['a']), wbTicket(3, ['a', 'b'])], false)).toBe('Whiteboard · 3 open · 2 assigned')
|
|
733
|
+
expect(whiteboardHeading([], false)).toBe('Whiteboard · 0 open · 0 assigned')
|
|
734
|
+
expect(whiteboardHeading([wbTicket(1)], true)).toBe('Whiteboard · 200+ open · 0 assigned')
|
|
735
|
+
expect(whiteboardHeading(null, false)).toBe('Whiteboard')
|
|
736
|
+
})
|
|
737
|
+
|
|
489
738
|
test('viewTarget is the viewed map, else the active one', () => {
|
|
490
739
|
expect(viewTarget(198, null)).toBe(198)
|
|
491
740
|
expect(viewTarget(198, 14)).toBe(14)
|
|
@@ -494,10 +743,10 @@ test('viewTarget is the viewed map, else the active one', () => {
|
|
|
494
743
|
})
|
|
495
744
|
|
|
496
745
|
test('nextViewed resets when the active map changed or the viewed map left the open list', () => {
|
|
497
|
-
const maps = [{ number: 14, title: 'a' }, { number: 198, title: 'b' }]
|
|
746
|
+
const maps = [{ number: 14, title: 'a', counts: null }, { number: 198, title: 'b', counts: null }]
|
|
498
747
|
expect(nextViewed(14, { activeChanged: false, maps })).toBe(14)
|
|
499
748
|
expect(nextViewed(14, { activeChanged: true, maps })).toBeNull()
|
|
500
|
-
expect(nextViewed(14, { activeChanged: false, maps: [{ number: 198, title: 'b' }] })).toBeNull()
|
|
749
|
+
expect(nextViewed(14, { activeChanged: false, maps: [{ number: 198, title: 'b', counts: null }] })).toBeNull()
|
|
501
750
|
expect(nextViewed(14, { activeChanged: false, maps: [] })).toBeNull()
|
|
502
751
|
// no list read yet (or a failed read): nothing to judge by
|
|
503
752
|
expect(nextViewed(14, { activeChanged: false, maps: null })).toBe(14)
|
|
@@ -505,7 +754,7 @@ test('nextViewed resets when the active map changed or the viewed map left the o
|
|
|
505
754
|
})
|
|
506
755
|
|
|
507
756
|
test('mapRowLayout marks the active map, bolds the viewed one and presses the others', () => {
|
|
508
|
-
const m = { number: 14, title: 'older map' }
|
|
757
|
+
const m = { number: 14, title: 'older map', counts: null }
|
|
509
758
|
expect(mapRowLayout(m, { active: 198, viewed: null, columns: 60 })).toEqual({
|
|
510
759
|
mark: ' ',
|
|
511
760
|
label: '#14 older map',
|
|
@@ -514,10 +763,10 @@ test('mapRowLayout marks the active map, bolds the viewed one and presses the ot
|
|
|
514
763
|
isViewed: false,
|
|
515
764
|
isPressable: true,
|
|
516
765
|
})
|
|
517
|
-
const act = mapRowLayout({ number: 198, title: 'trailhead mod' }, { active: 198, viewed: null, columns: 60 })
|
|
766
|
+
const act = mapRowLayout({ number: 198, title: 'trailhead mod', counts: null }, { active: 198, viewed: null, columns: 60 })
|
|
518
767
|
expect(act).toMatchObject({ mark: '● ', aside: ' active', isActive: true, isViewed: true, isPressable: false })
|
|
519
768
|
// viewing another map: the active one becomes pressable (back to active), the viewed one is not
|
|
520
|
-
expect(mapRowLayout({ number: 198, title: 't' }, { active: 198, viewed: 14, columns: 60 })).toMatchObject({
|
|
769
|
+
expect(mapRowLayout({ number: 198, title: 't', counts: null }, { active: 198, viewed: 14, columns: 60 })).toMatchObject({
|
|
521
770
|
isActive: true,
|
|
522
771
|
isViewed: false,
|
|
523
772
|
isPressable: true,
|
|
@@ -526,14 +775,14 @@ test('mapRowLayout marks the active map, bolds the viewed one and presses the ot
|
|
|
526
775
|
})
|
|
527
776
|
|
|
528
777
|
test('mapRowLayout cuts the title to the room left by the mark and the aside', () => {
|
|
529
|
-
const long = { number: 198, title: 'x'.repeat(200) }
|
|
778
|
+
const long = { number: 198, title: 'x'.repeat(200), counts: null }
|
|
530
779
|
const act = mapRowLayout(long, { active: 198, viewed: null, columns: 30 })
|
|
531
780
|
expect(act.label.endsWith('…')).toBe(true)
|
|
532
781
|
expect(act.mark.length + act.label.length + (act.aside?.length ?? 0)).toBeLessThanOrEqual(30)
|
|
533
|
-
const other = mapRowLayout({ number: 14, title: 'x'.repeat(200) }, { active: 198, viewed: null, columns: 30 })
|
|
782
|
+
const other = mapRowLayout({ number: 14, title: 'x'.repeat(200), counts: null }, { active: 198, viewed: null, columns: 30 })
|
|
534
783
|
// a Button draws `[ label ]`: four cells of chrome
|
|
535
784
|
expect(other.mark.length + other.label.length + 4).toBeLessThanOrEqual(30)
|
|
536
|
-
expect(mapRowLayout({ number: 14, title: '' }, { active: 198, viewed: null, columns: 30 }).label).toBe('#14')
|
|
785
|
+
expect(mapRowLayout({ number: 14, title: '', counts: null }, { active: 198, viewed: null, columns: 30 }).label).toBe('#14')
|
|
537
786
|
})
|
|
538
787
|
|
|
539
788
|
test('footerStatus keeps a status that fits beside the two buttons on their row', () => {
|
|
@@ -563,3 +812,42 @@ test('footerStatus on a pane too narrow for the buttons never throws', () => {
|
|
|
563
812
|
expect(footerStatus('updated just now', 10)).toEqual({ text: 'updated j…', isInline: false })
|
|
564
813
|
expect(footerStatus('updated just now', 0)).toEqual({ text: '…', isInline: false })
|
|
565
814
|
})
|
|
815
|
+
|
|
816
|
+
test('parseTickets on the whiteboard keeps only the whiteboard rows, unverified ones still counted', () => {
|
|
817
|
+
const rows = [
|
|
818
|
+
{ number: 1, title: 'map one', labels: [{ name: 'trailhead:build' }], assignees: [], url: '' },
|
|
819
|
+
{ number: 2, title: 'loose', labels: [{ name: 'trailhead:whiteboard' }, { name: 'trailhead:bug' }], assignees: [], url: '' },
|
|
820
|
+
{ number: 3, title: 'held', labels: [{ name: 'trailhead:whiteboard' }], assignees: [{ login: 'al' }], url: '' },
|
|
821
|
+
{ number: 4, title: 'q', labels: [{ name: 'trailhead:whiteboard' }, { name: 'trailhead:unverified' }], assignees: [], url: '' },
|
|
822
|
+
]
|
|
823
|
+
const parsed = parseTickets(JSON.stringify(rows), { isWhiteboard: true })
|
|
824
|
+
expect(parsed?.tickets.map(t => [t.number, t.type, t.state])).toEqual([
|
|
825
|
+
[2, 'bug', 'frontier'],
|
|
826
|
+
[3, null, 'in-progress'],
|
|
827
|
+
])
|
|
828
|
+
expect(parsed?.unverifiedCount).toBe(1)
|
|
829
|
+
expect(parseTickets(JSON.stringify(rows))?.tickets.map(t => t.number)).toEqual([1])
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
test('parseDashboard takes the lowest numbered index, none when the repo has none, null when unreadable', () => {
|
|
833
|
+
expect(parseDashboard(JSON.stringify([{ number: 9, url: 'https://x/9' }, { number: 5, url: 'https://x/5' }]))).toEqual({
|
|
834
|
+
ref: { number: 5, url: 'https://x/5' },
|
|
835
|
+
})
|
|
836
|
+
expect(parseDashboard(JSON.stringify([{ number: 5, url: 'javascript:alert(1)' }]))).toEqual({ ref: { number: 5, url: '' } })
|
|
837
|
+
expect(parseDashboard('[]')).toEqual({ ref: null })
|
|
838
|
+
expect(parseDashboard('nope')).toBeNull()
|
|
839
|
+
expect(parseDashboard('{}')).toBeNull()
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
test('mapRowLayout on the dashboard makes every map pressable, the active one still marked', () => {
|
|
843
|
+
const r = mapRowLayout({ number: 198, title: 'trailhead mod', counts: null }, { active: 198, viewed: null, columns: 60, isDashboard: true })
|
|
844
|
+
expect(r).toMatchObject({ isActive: true, isViewed: false, isPressable: true, mark: '● ', aside: ' active' })
|
|
845
|
+
expect(mapRowLayout({ number: 198, title: 'trailhead mod', counts: null }, { active: 198, viewed: null, columns: 60 }).isPressable).toBe(false)
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
test('headingRoom leaves the switch button its cells, never below zero', () => {
|
|
849
|
+
// `[ dashboard ]` is 13 cells, plus one gap
|
|
850
|
+
expect(headingRoom(60, 'dashboard')).toBe(46)
|
|
851
|
+
expect(headingRoom(60, 'map')).toBe(52)
|
|
852
|
+
expect(headingRoom(5, 'dashboard')).toBe(0)
|
|
853
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from 'claude-code/testing'
|
|
2
2
|
|
|
3
|
-
import { fit, parseWorkState, pipeline, sameValue, statusLine } from '../hooks/mod/work-state-view'
|
|
3
|
+
import { fit, nextOwnTicket, parseWorkState, pipeline, sameValue, statusLine } from '../hooks/mod/work-state-view'
|
|
4
4
|
|
|
5
5
|
const valid = {
|
|
6
6
|
map: 198,
|
|
@@ -84,3 +84,27 @@ test('fit cuts with an ellipsis by code point and leaves a short text whole', ()
|
|
|
84
84
|
expect(fit('abcdef', 1)).toBe('…')
|
|
85
85
|
expect(fit('a' + '\u{1F680}'.repeat(5), 3)).toBe('a\u{1F680}…')
|
|
86
86
|
})
|
|
87
|
+
|
|
88
|
+
// nextOwnTicket(prev, lastState, nextState, session): the ticket this session owns across a shared file.
|
|
89
|
+
const stamped = (ticket: number, session: string | null) => ({ ...valid, ticket, session })
|
|
90
|
+
|
|
91
|
+
test('nextOwnTicket takes the ticket of a state stamped with this session', () => {
|
|
92
|
+
expect(nextOwnTicket(null, null, stamped(259, 's1'), 's1')).toBe(259)
|
|
93
|
+
expect(nextOwnTicket(212, stamped(212, 's1'), stamped(259, 's1'), 's1')).toBe(259)
|
|
94
|
+
// a state with no session is this one's, like the orphan rule
|
|
95
|
+
expect(nextOwnTicket(null, null, stamped(259, null), 's1')).toBe(259)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('nextOwnTicket leaves the ticket alone when another session wrote the file', () => {
|
|
99
|
+
expect(nextOwnTicket(259, stamped(259, 's1'), stamped(212, 's2'), 's1')).toBe(259)
|
|
100
|
+
expect(nextOwnTicket(null, null, stamped(212, 's2'), 's1')).toBeNull()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('nextOwnTicket clears on a missing file only when the last state read was this session\'s', () => {
|
|
104
|
+
expect(nextOwnTicket(259, stamped(259, 's1'), null, 's1')).toBeNull()
|
|
105
|
+
expect(nextOwnTicket(259, stamped(259, null), null, 's1')).toBeNull()
|
|
106
|
+
// own state, then another session's, then a missing file: the last read was not ours
|
|
107
|
+
expect(nextOwnTicket(259, stamped(212, 's2'), null, 's1')).toBe(259)
|
|
108
|
+
expect(nextOwnTicket(259, null, null, 's1')).toBe(259)
|
|
109
|
+
expect(nextOwnTicket(null, null, null, 's1')).toBeNull()
|
|
110
|
+
})
|
|
@@ -15,7 +15,10 @@ export type PromptSnapshot = { key: string; text: string | null }
|
|
|
15
15
|
|
|
16
16
|
// The map pane (#208): what the last `gh` read of the active map gave.
|
|
17
17
|
export type TicketType = 'decision' | 'research' | 'prototype' | 'build' | 'bug' | 'task'
|
|
18
|
-
export type TicketState = 'frontier' | 'in-progress' | 'blocked'
|
|
18
|
+
export type TicketState = 'frontier' | 'in-progress' | 'paused' | 'blocked'
|
|
19
|
+
|
|
20
|
+
// The parent a pending UAT ticket tests (from its `UAT of:` body line); title is empty for a bare `#<n>`.
|
|
21
|
+
export type UatParent = { number: number; title: string }
|
|
19
22
|
|
|
20
23
|
export type PaneTicket = {
|
|
21
24
|
number: number
|
|
@@ -24,10 +27,19 @@ export type PaneTicket = {
|
|
|
24
27
|
state: TicketState
|
|
25
28
|
assignees: readonly string[]
|
|
26
29
|
url: string
|
|
30
|
+
// Carries `trailhead:uat`: a marker beside `state`, only the grouping moves it (paused still wins).
|
|
31
|
+
isUat: boolean
|
|
32
|
+
// The parent it tests, once read; null when unknown (no body read, or no usable `UAT of:` line).
|
|
33
|
+
uatOf: UatParent | null
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
//
|
|
30
|
-
export type
|
|
36
|
+
// A map's sub-issue counts: `open` still to do, `total` all of them.
|
|
37
|
+
export type MapCounts = { open: number; total: number }
|
|
38
|
+
// One open map of the repo, as the maps list shows it; `counts` null = unknown, drawn name-only.
|
|
39
|
+
export type MapSummary = { number: number; title: string; counts: MapCounts | null }
|
|
40
|
+
|
|
41
|
+
// What the maps read gives: the open maps and the whiteboard's counts (null when they were unreadable).
|
|
42
|
+
export type MapsRead = { maps: readonly MapSummary[]; whiteboard: MapCounts | null }
|
|
31
43
|
|
|
32
44
|
export type MapInfo = { number: number; title: string; destination: string | null; url: string }
|
|
33
45
|
|
|
@@ -45,6 +57,8 @@ export type MapPaneData = {
|
|
|
45
57
|
isCapped: boolean
|
|
46
58
|
// The repo's open maps (empty until a first good read, then the last good one).
|
|
47
59
|
maps: readonly MapSummary[]
|
|
60
|
+
// The whiteboard's open and total tickets; null = unknown, the Whiteboard row is drawn name-only.
|
|
61
|
+
whiteboard: MapCounts | null
|
|
48
62
|
history: BurndownHistory | null
|
|
49
63
|
historyAt: number | null
|
|
50
64
|
fetchedAt: number | null
|
|
@@ -52,16 +66,36 @@ export type MapPaneData = {
|
|
|
52
66
|
isLoading: boolean
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
// The pinned per-repo index issue (`trailhead:dashboard`), as the pane links it.
|
|
70
|
+
export type DashboardRef = { number: number; url: string }
|
|
71
|
+
|
|
72
|
+
// The pane's dashboard view: the whiteboard tickets (map-less) and the pinned index; the open maps
|
|
73
|
+
// stay in `MapPaneData.maps`. Same failure rule: the last good read is kept, `error` says why.
|
|
74
|
+
export type DashboardPaneData = {
|
|
75
|
+
dashboard: DashboardRef | null
|
|
76
|
+
tickets: readonly PaneTicket[]
|
|
77
|
+
unverifiedCount: number
|
|
78
|
+
isCapped: boolean
|
|
79
|
+
fetchedAt: number | null
|
|
80
|
+
error: string | null
|
|
81
|
+
isLoading: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
declare module 'claude-code' {
|
|
56
85
|
interface PluginState {
|
|
57
86
|
trailhead: {
|
|
58
87
|
workState: WorkState | null
|
|
59
88
|
isOrphaned: boolean
|
|
89
|
+
// The ticket this session works, kept when another session's state takes over the shared file.
|
|
90
|
+
ownTicket: number | null
|
|
60
91
|
promptSnapshot: PromptSnapshot | null
|
|
61
92
|
mapPane: MapPaneData
|
|
62
93
|
activeMap: number | null
|
|
63
94
|
// The map the pane shows when it is not the active one; null follows the active map.
|
|
64
95
|
viewedMap: number | null
|
|
96
|
+
// The pane shows the dashboard (maps index + whiteboard) instead of a map.
|
|
97
|
+
isDashboard: boolean
|
|
98
|
+
dashboardPane: DashboardPaneData
|
|
65
99
|
pendingWork: number | null
|
|
66
100
|
paneAutoOpened: boolean
|
|
67
101
|
}
|