@marcomigozzi/trailhead 0.17.0 → 0.18.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 +28 -18
- package/README.md +28 -18
- package/bin/base-preflight-docs.test.js +4 -1
- package/bin/bulk-cross-reference-docs.test.js +36 -0
- package/bin/engine-route-docs.test.js +7 -0
- package/bin/facet-stamp-docs.test.js +79 -0
- package/bin/forced-mutation-docs.test.js +38 -0
- package/bin/guard-axis-docs.test.js +69 -0
- package/bin/inbox-needs-info-docs.test.js +68 -0
- package/bin/lib/codex-projection.js +8 -8
- package/bin/lib/codex-projection.test.js +19 -3
- package/bin/needs-you-docs.test.js +9 -2
- package/bin/options-in-chat-docs.test.js +65 -0
- package/bin/pick-stress-test-docs.test.js +1 -1
- package/bin/pipeline-review-docs.test.js +118 -0
- package/bin/plan-review-budget-docs.test.js +63 -0
- package/bin/repo-target-docs.test.js +41 -0
- package/bin/seam-coverage-docs.test.js +46 -0
- package/bin/standards-brief-docs.test.js +112 -0
- package/bin/test-once-docs.test.js +101 -0
- package/bin/test-run-docs.test.js +47 -0
- package/bin/trailhead.js +9 -4
- package/bin/trailhead.test.js +18 -6
- package/bin/yes-accepts-recommended-docs.test.js +54 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
- package/plugins/trailhead/agents/trailhead-grill.md +2 -2
- package/plugins/trailhead/agents/trailhead-plan.md +1 -1
- package/plugins/trailhead/agents/trailhead-verify.md +1 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
- package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
- package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
- package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
- package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
- package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
- package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +81 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
- package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
- package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
- package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
- package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
- package/plugins/trailhead/hooks/mod/README.md +30 -14
- package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
- package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
- package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
- package/plugins/trailhead/hooks/mod/map-pane.tsx +144 -45
- package/plugins/trailhead/hooks/mod/repo-pin.ts +105 -4
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
- package/plugins/trailhead/hooks/trailhead-check-update.js +3 -1
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
- package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
- package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +5 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +12 -0
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/substrate-commands.md +11 -2
- package/plugins/trailhead/skills/_shared/substrate.md +3 -2
- package/plugins/trailhead/skills/_shared/teamwork.md +5 -3
- package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +18 -5
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/debug.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +3 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
- package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques.md +3 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +8 -3
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +17 -2
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +7 -7
- package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
- package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
- package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
- package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
- package/plugins/trailhead/templates/trailhead-capture.js +2 -1
- package/plugins/trailhead/templates/trailhead-checkout-find.js +3 -2
- package/plugins/trailhead/templates/trailhead-checkout-in.js +2 -1
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
- package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
- package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
- package/plugins/trailhead/templates/trailhead-issue-body.js +92 -6
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
- package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
- package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
- package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
- package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
- package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
- package/plugins/trailhead/tests/git-env.test.ts +20 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +268 -17
- package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
- package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
- package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
- package/plugins/trailhead/tests/unscrub.ts +8 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
- package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Repo-location environment for git spawns.
|
|
3
|
+
//
|
|
4
|
+
// Git gives these variables precedence over `-C` and the cwd, so an exported
|
|
5
|
+
// GIT_DIR (a hook, `git rebase --exec`, a parent shell) silently redirects every
|
|
6
|
+
// git call to another repository. Each git spawn in the plugin builds its env
|
|
7
|
+
// through gitEnv() so it always acts on the checkout it was pointed at.
|
|
8
|
+
//
|
|
9
|
+
// The names are `git rev-parse --local-env-vars` minus GIT_CONFIG*: those carry
|
|
10
|
+
// the parent's `-c` options and are meant to be inherited. A static list keeps
|
|
11
|
+
// this free of a git spawn; refresh it from that command when git adds a name.
|
|
12
|
+
// Names must be deleted: an empty string is not "unset" (`fatal: '' is not a git repository`).
|
|
13
|
+
//
|
|
14
|
+
// hooks/lib/trailhead-git-env.js is a byte-identical copy for the hooks (they
|
|
15
|
+
// install without the templates); a test keeps the two in sync.
|
|
16
|
+
|
|
17
|
+
const GIT_LOCATION_VARS = Object.freeze([
|
|
18
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
19
|
+
'GIT_OBJECT_DIRECTORY',
|
|
20
|
+
'GIT_DIR',
|
|
21
|
+
'GIT_WORK_TREE',
|
|
22
|
+
'GIT_IMPLICIT_WORK_TREE',
|
|
23
|
+
'GIT_GRAFT_FILE',
|
|
24
|
+
'GIT_INDEX_FILE',
|
|
25
|
+
'GIT_NO_REPLACE_OBJECTS',
|
|
26
|
+
'GIT_REPLACE_REF_BASE',
|
|
27
|
+
'GIT_PREFIX',
|
|
28
|
+
'GIT_SHALLOW_FILE',
|
|
29
|
+
'GIT_COMMON_DIR',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// A new env object: `base` without the repo-location names, then `extra` on top.
|
|
33
|
+
// Never mutates `base` (process.env by default).
|
|
34
|
+
function gitEnv(base = process.env, extra = {}) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [k, v] of Object.entries(base)) if (!GIT_LOCATION_VARS.includes(k)) out[k] = v;
|
|
37
|
+
return { ...out, ...extra };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { GIT_LOCATION_VARS, gitEnv };
|
|
@@ -97,8 +97,12 @@ only where no mod does (it says the panel is not available). Claude Code only: t
|
|
|
97
97
|
projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex-projection.js`).
|
|
98
98
|
|
|
99
99
|
- **What it shows**: the map as a link with its destination (dim), then `In progress · n` (first, right under
|
|
100
|
-
the maps list), `Needs you · n`, `Frontier · n`, `UAT · n`, `Paused · n` and `Blocked · n` (an empty group is left out).
|
|
101
|
-
|
|
100
|
+
the maps list), `Needs you · n`, `Frontier · n`, `UAT · n`, `Paused · n` and `Blocked · n` (an empty group is left out). Each group, the
|
|
101
|
+
maps list included, has its own glyph and colour (`◈` Maps violet, `▶` In progress accent, `!` Needs you orange, `◆` Frontier green,
|
|
102
|
+
`◎` UAT cyan, `‖` Paused grey, `■` Blocked red): the heading is the glyph, the bold name in that colour and a dim rule to the pane's edge, and
|
|
103
|
+
every ticket's block (its row and the dim lines under it) sits after a gutter in the same colour, so the groups tell apart without
|
|
104
|
+
colour too: in the terminal a `│ ` on each of its lines, on the desktop one bar as tall as the block; the lines under the row are indented past it. A row is `#n title`
|
|
105
|
+
(a link to the issue) and the type (`build` cyan, `decision` orange, any other dim; dim on a blocked row). The unverified-count line is amber. Every assigned row shows its holder as `@login`; the ticket
|
|
102
106
|
in play in this session is marked `▸` in the accent and says `this session` in place of its holder;
|
|
103
107
|
blocked rows are dim. `In progress` is global for you: after the shown scope's own claimed tickets it lists your
|
|
104
108
|
open in-progress tickets (assigned to the `gh` login) of every other open map and of the whiteboard, each with a dim
|
|
@@ -141,7 +145,7 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
|
|
|
141
145
|
did not change the session, runs no work command. `trailhead:unverified` tickets are never listed,
|
|
142
146
|
only counted in one dim line; `trailhead:whiteboard` tickets are dropped. The footer is a `refresh`
|
|
143
147
|
button and `updated <age>`, or `⚠ <reason> · list from <age>` after a `gh` failure (the last good list is kept;
|
|
144
|
-
reasons: `gh not installed`, `gh not authenticated (run gh auth login)`,
|
|
148
|
+
reasons: `gh not installed`, `checkout not found: <path>` (the pinned folder is gone and no other checkout of the repo was found), `gh not authenticated (run gh auth login)`,
|
|
145
149
|
`offline: cannot reach GitHub`, `gh failed (exit <n>)`), and a `close` button at the right edge that
|
|
146
150
|
closes the panel. A status too long for the room beside the buttons moves to a row of its own under them. Titles come from GitHub and are untrusted:
|
|
147
151
|
flattened, stripped of control, zero-width and bidi characters, only drawn. Other states: `Loading tickets…`
|
|
@@ -252,14 +256,17 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
|
|
|
252
256
|
from it), and `.trailhead/active-map` and `.trailhead/work-state.json` are read by absolute path under
|
|
253
257
|
it, so a shell `cd` out of the repo changes nothing. A check outside any repo, or in one with no usable
|
|
254
258
|
remote, keeps the pin; a move into another repo moves it, drops the reads in flight and empties what the
|
|
255
|
-
pane held for the old one.
|
|
259
|
+
pane held for the old one. A pinned checkout deleted since (a finished worktree) moves the pin, silently, to a live checkout of
|
|
260
|
+
the same repo: the main working tree, else another `git worktree`, else a same-remote clone beside it (probes run as `git -C`
|
|
261
|
+
from `/`, retried at most once a minute). With nothing ever pinned the pane says
|
|
256
262
|
`Not in a GitHub repository: open the session in a trailhead repo` and makes no `gh` call.
|
|
257
263
|
- **When it refreshes**: only while the pane is open (a closed pane costs no `gh` call). Opening it,
|
|
258
264
|
`session.start`, `turn.complete`, a Bash call that writes to the tracker, the `refresh` button, and a slow
|
|
259
265
|
timer started on open. A write is `gh issue edit|close|create|comment|reopen|delete|transfer`, a `gh api` call on a
|
|
260
266
|
`repos/.../issues` path with a write method (`-X`/`--method` POST, PATCH, PUT, DELETE) or an implicit POST
|
|
261
267
|
(`-f`, `-F`, `--field`, `--raw-field`, `--input`) and no `-X GET`, or the `trailhead-issue-body.js` helper
|
|
262
|
-
(`GH_ISSUE_WRITE`)
|
|
268
|
+
(`GH_ISSUE_WRITE`), outside any heredoc body (`isTrackerWrite`: a body is data, so a `gh issue close` line in a
|
|
269
|
+
`cat <<EOF` writes nothing); reads never refresh. Never two fetches inside `MIN_REFRESH_MS` (save the turn-end read below): a request inside the floor
|
|
263
270
|
keeps one trailing refresh for the rest of it. A write that reads at once also arms that trailing refresh, so one
|
|
264
271
|
more read follows when the floor ends, after the search index (the history) caught up; a burst of writes still costs one
|
|
265
272
|
read per floor plus one trailing, and one more when the turn ends after it. The host drops a refused timer without a word (a refused `every` period ends
|
|
@@ -271,16 +278,25 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
|
|
|
271
278
|
involved; it leaves an armed trailing refresh alone, so the read after the search index caught up still follows.
|
|
272
279
|
A read asked while the pane is undrawn is dropped; a render
|
|
273
280
|
whose list (with no error) is older than `TIMER_MS + TIMER_GRACE_MS` asks for a read at once, as an empty view does.
|
|
274
|
-
- **Auto-open**:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
pane
|
|
281
|
+
- **Auto-open**: in fullscreen (`e.viewport.isFullscreen === true` in the `AbovePrompt` render, or in the
|
|
282
|
+
viewport of a `session.attach`, so a surface that attaches later tries again), when a map is active. Never on
|
|
283
|
+
the main screen, never again after the person acted on the pane (the command, closing it by hand, the `close`
|
|
284
|
+
button). The open runs from a zero-delay timer because a render may not write state.
|
|
285
|
+
The auto-open counts as done only once the pane is actually drawn (seen placed by a later check, or by the pane's
|
|
286
|
+
own render, which fires only when drawn). Being unasked, the open can stay undrawn (`isPlaced: false`): below
|
|
287
|
+
144 columns, or on a surface that has not reported its layout yet, typical of a fresh session. That does not burn
|
|
288
|
+
the auto-open: while the pane is listed undrawn nothing opens it again (the host places it itself when it can),
|
|
289
|
+
and if it vanishes undrawn the next trigger opens it again. `/trailhead:panel` closes a pane only when it is
|
|
290
|
+
listed and drawn; a listed but undrawn pane is opened again as the person's request (drawn at any width), and if
|
|
291
|
+
even that leaves it undrawn the pane stays open and the command says so in a fixed sentence (`Map panel is open but not shown yet: ...`; the host's reason goes to the
|
|
292
|
+
debug log only), so it is never closed unseen. With no surface attached yet (`no-surface`, the start of a session) the command answers
|
|
293
|
+
that the panel will open once the screen is ready, again on every repeat; the pane's first render on the surface
|
|
294
|
+
that attaches asks for its read. A desktop session starts at its first message, not when it is created, so nothing
|
|
295
|
+
can open the pane on a session where nothing was typed, and a `/trailhead:panel` sent as that first message lands
|
|
296
|
+
right after the auto-open. Within `UNSEEN_MS` of a drawn auto-open the command keeps the pane and answers
|
|
297
|
+
`Map panel opened.` instead of closing a pane nobody could have seen.
|
|
282
298
|
- **Constants** (`map-pane.tsx`): `PANE_ID = 'trailhead-map'`, `PANE_TITLE = 'Map'`,
|
|
283
|
-
`MIN_REFRESH_MS = 15_000`, `TIMER_MS = 180_000`, `GH_TIMEOUT_MS = 20_000`, `TIMER_GRACE_MS = 30_000`,
|
|
299
|
+
`MIN_REFRESH_MS = 15_000`, `TIMER_MS = 180_000`, `GH_TIMEOUT_MS = 20_000`, `TIMER_GRACE_MS = 30_000`, `UNSEEN_MS = 2_000`,
|
|
284
300
|
`PANEL_COMMAND = 'trailhead:panel'`, `MAPS_LIMIT = MAPS_READ_LIMIT = 100` (`MAPS_LIMIT` not exported); `worktree-porcelain.ts`: `MAX_FAILURES = 3` (shared by the work-state lookup and the run scan); `auto-run-view.ts`: `RESUME_HINT`; `map-pane-view.ts`: `MAPS_READ_LIMIT = 100`, `LIST_LIMIT = 200`, `HISTORY_LIMIT = 5000`.
|
|
285
301
|
- **Atoms**: `mapPane` (with `maps`, the open maps), `activeMap`, `viewedMap` (`null` follows the active map), `isDashboard`, `dashboardPane`, `pendingWork`, `pendingPhase` (`queued` or `starting`), `paneAutoOpened`. Module variables hold the queue and
|
|
286
302
|
the timers, what `/clear` carries over (`carried`), the held press (`held`) and the one being sent (`inFlight`),
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { EngineInterface } from 'claude-code'
|
|
5
5
|
|
|
6
6
|
import type { AutoRun, RepoPin, RunCounts, WorkState } from '../../types'
|
|
7
|
+
import { gitArgv } from './git-env'
|
|
7
8
|
import { GH_TIMEOUT_MS, MIN_REFRESH_MS } from './map-pane'
|
|
8
9
|
import { aggregateRun, isRunMember, nextRunAfterFailure, parseRunCounts, parseRunScope, runCountsArgs, slotTicket } from './auto-run-view'
|
|
9
10
|
import type { Where } from './auto-run-view'
|
|
@@ -48,7 +49,7 @@ async function gate($: EngineInterface, main: string, own: WorkState | null): Pr
|
|
|
48
49
|
// La regola di cloneOnBranch (trailhead-auto-slot.js): git dir dentro la cartella e HEAD sul branch dello slot.
|
|
49
50
|
// undefined: git non è riuscito a girare.
|
|
50
51
|
async function cloneOnBranch($: EngineInterface, dir: string, ticket: number): Promise<boolean | undefined> {
|
|
51
|
-
const git = (args: string[]) => $.process.run(
|
|
52
|
+
const git = (args: string[]) => $.process.run(gitArgv('-C', dir, ...args), { cwd: dir, timeoutMs: GIT_TIMEOUT_MS })
|
|
52
53
|
try {
|
|
53
54
|
const g = await git(['rev-parse', '--absolute-git-dir'])
|
|
54
55
|
if (g.exitCode !== 0) return false
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Leaf without imports: git argv for the mod's process spawns.
|
|
2
|
+
//
|
|
3
|
+
// Git gives the repo-location variables precedence over `-C` and the cwd, and
|
|
4
|
+
// `$.process.run` can only add or override env, never delete. So an inherited
|
|
5
|
+
// GIT_DIR would redirect every git spawn: wrap the argv in `env -u NAME ...`.
|
|
6
|
+
// The list is templates/trailhead-git-env.js's GIT_LOCATION_VARS (git rev-parse
|
|
7
|
+
// --local-env-vars minus GIT_CONFIG*); hooks/lib/git-env-mod.test.js keeps them equal.
|
|
8
|
+
export const GIT_LOCATION_VARS: readonly string[] = [
|
|
9
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
10
|
+
'GIT_OBJECT_DIRECTORY',
|
|
11
|
+
'GIT_DIR',
|
|
12
|
+
'GIT_WORK_TREE',
|
|
13
|
+
'GIT_IMPLICIT_WORK_TREE',
|
|
14
|
+
'GIT_GRAFT_FILE',
|
|
15
|
+
'GIT_INDEX_FILE',
|
|
16
|
+
'GIT_NO_REPLACE_OBJECTS',
|
|
17
|
+
'GIT_REPLACE_REF_BASE',
|
|
18
|
+
'GIT_PREFIX',
|
|
19
|
+
'GIT_SHALLOW_FILE',
|
|
20
|
+
'GIT_COMMON_DIR',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
// `argv` run with the repo-location variables removed from its env.
|
|
24
|
+
export const scrubGitEnv = (argv: readonly string[]): string[] => [
|
|
25
|
+
'env',
|
|
26
|
+
...GIT_LOCATION_VARS.flatMap(name => ['-u', name]),
|
|
27
|
+
...argv,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
// `git <args>` that acts on the checkout it is pointed at (-C / cwd), never on an inherited GIT_DIR.
|
|
31
|
+
export const gitArgv = (...args: readonly string[]): string[] => scrubGitEnv(['git', ...args])
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Leaf without imports: heredoc bodies out of a Bash command (#369).
|
|
2
|
+
//
|
|
3
|
+
// A heredoc body is data (a file being written), not commands, so a `gh issue close` line inside
|
|
4
|
+
// one is no tracker write. Only the opener line stays, with every body and terminator line dropped.
|
|
5
|
+
// The mod imports nothing from hooks/lib, so this is a small twin of shell-scan.js's
|
|
6
|
+
// stripHeredocBodies; hooks/lib/heredoc-mod.test.js keeps their output equal.
|
|
7
|
+
// When it cannot tell, a line is kept: a lost write leaves a stale pane, an extra read costs one `gh` call.
|
|
8
|
+
// Known difference: an opener inside `$(..)` is not seen here (shell-scan sees it), so those lines stay.
|
|
9
|
+
|
|
10
|
+
// The heredoc openers of one line, in order: [{ tag, isDash }]. A `<<` counts outside quotes and outside
|
|
11
|
+
// `((..))` arithmetic, and not as a here-string (`<<<`); an opener with no word after it is no opener.
|
|
12
|
+
// The scan stops at an unquoted `#` that starts a comment (line start, or after whitespace or `;&|(`).
|
|
13
|
+
// `quoteIn` is the quote still open from an earlier line; the quote open at the end of the line is returned.
|
|
14
|
+
function openersOf(
|
|
15
|
+
line: string,
|
|
16
|
+
quoteIn: string,
|
|
17
|
+
): { found: Array<{ tag: string; isDash: boolean }>; quote: string } {
|
|
18
|
+
const found: Array<{ tag: string; isDash: boolean }> = []
|
|
19
|
+
let quote = quoteIn
|
|
20
|
+
let arith = 0
|
|
21
|
+
for (let i = 0; i < line.length; i++) {
|
|
22
|
+
const c = line[i]
|
|
23
|
+
if (quote !== '') {
|
|
24
|
+
if (c === '\\' && quote === '"') i++
|
|
25
|
+
else if (c === quote) quote = ''
|
|
26
|
+
} else if (c === '\\') {
|
|
27
|
+
i++
|
|
28
|
+
} else if (line.startsWith('${', i)) {
|
|
29
|
+
// A parameter expansion holds no opener (`${x:-<<EOF}`); skip to its `}`, or the rest of the line.
|
|
30
|
+
const end = line.indexOf('}', i)
|
|
31
|
+
if (end < 0) break
|
|
32
|
+
i = end
|
|
33
|
+
} else if (c === '#' && (i === 0 || /[\s;&|(]/.test(line[i - 1]))) {
|
|
34
|
+
break
|
|
35
|
+
} else if (c === "'" || c === '"') {
|
|
36
|
+
quote = c
|
|
37
|
+
} else if (line.startsWith('((', i)) {
|
|
38
|
+
arith++
|
|
39
|
+
i++
|
|
40
|
+
} else if (arith > 0 && line.startsWith('))', i)) {
|
|
41
|
+
arith--
|
|
42
|
+
i++
|
|
43
|
+
} else if (arith === 0 && line.startsWith('<<', i)) {
|
|
44
|
+
if (line[i + 2] === '<') {
|
|
45
|
+
i += 2
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
const isDash = line[i + 2] === '-'
|
|
49
|
+
const word = /^\s*((?:\\.|'[^']*'|"[^"]*"|[^\s;&|<>()'"\\])+)/.exec(line.slice(i + (isDash ? 3 : 2)))
|
|
50
|
+
if (word !== null) {
|
|
51
|
+
found.push({ tag: word[1].replace(/\\(.)|['"]/g, '$1'), isDash })
|
|
52
|
+
i += (isDash ? 3 : 2) + word[0].length - 1
|
|
53
|
+
} else {
|
|
54
|
+
i++
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { found, quote }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Every line from an opener to its terminator (the terminator included) goes; a body that never ends
|
|
62
|
+
// takes the rest of the command, as the shell reads it.
|
|
63
|
+
export function stripHeredocBodies(command: string): string {
|
|
64
|
+
const lines = command.split('\n')
|
|
65
|
+
const kept: string[] = []
|
|
66
|
+
let quote = ''
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
kept.push(lines[i])
|
|
69
|
+
const scan = openersOf(lines[i], quote)
|
|
70
|
+
quote = scan.quote
|
|
71
|
+
for (const { tag, isDash } of scan.found) {
|
|
72
|
+
while (i + 1 < lines.length) {
|
|
73
|
+
i++
|
|
74
|
+
if ((isDash ? lines[i].replace(/^\t+/, '') : lines[i]) === tag) break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return kept.join('\n')
|
|
79
|
+
}
|
|
@@ -15,7 +15,8 @@ export type TicketGroups = {
|
|
|
15
15
|
paused: readonly PaneTicket[]
|
|
16
16
|
blocked: readonly PaneTicket[]
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
// `missingRoot`: the folder `gh` was to run in, when it no longer exists (a spawn there fails like a missing binary).
|
|
19
|
+
export type Failure = { error: unknown; missingRoot?: string } | { exitCode: number; stderr: string }
|
|
19
20
|
export type RefreshDecision = { run: true } | { run: false; waitMs: number }
|
|
20
21
|
|
|
21
22
|
// The most rows one `gh issue list` is asked for.
|
|
@@ -429,6 +430,7 @@ const errorText = (err: unknown): string => {
|
|
|
429
430
|
// One short line saying why `gh` gave nothing.
|
|
430
431
|
export const failureReason = (failure: Failure): string => {
|
|
431
432
|
if ('error' in failure) {
|
|
433
|
+
if (failure.missingRoot !== undefined) return `checkout not found: ${failure.missingRoot}`
|
|
432
434
|
const text = errorText(failure.error)
|
|
433
435
|
if (/ENOENT|not found/i.test(text)) return 'gh not installed'
|
|
434
436
|
if (/timed? ?out/i.test(text)) return 'offline: cannot reach GitHub'
|
|
@@ -50,6 +50,8 @@ import {
|
|
|
50
50
|
} from './map-pane-view'
|
|
51
51
|
import { elsewhereInProgress, myTicketsFromRows, parseLogin, scopeLine } from './my-tickets'
|
|
52
52
|
import { checkoutArg, currentRepo, isPinCheckout, isSamePin, pinKey, pinPath, refreshPin, toplevelOf } from './repo-pin'
|
|
53
|
+
import { gitArgv, scrubGitEnv } from './git-env'
|
|
54
|
+
import { stripHeredocBodies } from './heredoc'
|
|
53
55
|
import { isMainTurnRunning } from './turn-state'
|
|
54
56
|
import { createdIssueNumber, isWhiteboardCreate, nextFollow, whiteboardHint } from './whiteboard-follow'
|
|
55
57
|
import { fit, isMissing, sameValue } from './work-state-view'
|
|
@@ -92,6 +94,24 @@ const CHART_ROWS = 3
|
|
|
92
94
|
// Space between ticket rows on the desktop, whose native buttons would touch; the terminal stays compact.
|
|
93
95
|
const DESKTOP_ROW_GAP = 0.5
|
|
94
96
|
const AMBER = '#e0af68'
|
|
97
|
+
// Ogni sezione ha il suo glifo e colore (palette Tokyo Night del band), così si distinguono anche senza colore.
|
|
98
|
+
const SECTION_STYLES = {
|
|
99
|
+
maps: { glyph: '◈', color: '#bb9af7' },
|
|
100
|
+
'in-progress': { glyph: '▶', color: ACCENT },
|
|
101
|
+
'needs-you': { glyph: '!', color: '#ff9e64' },
|
|
102
|
+
frontier: { glyph: '◆', color: '#9ece6a' },
|
|
103
|
+
uat: { glyph: '◎', color: '#7dcfff' },
|
|
104
|
+
paused: { glyph: '‖', color: '#a9b1d6' },
|
|
105
|
+
blocked: { glyph: '■', color: '#f7768e' },
|
|
106
|
+
} as const
|
|
107
|
+
type SectionKey = keyof typeof SECTION_STYLES
|
|
108
|
+
// Il filetto a sinistra di ogni riga, nel colore della sezione: occupa GUTTER colonne.
|
|
109
|
+
const GUTTER = '│ '
|
|
110
|
+
// Sul desktop il filetto è una barra continua di questa larghezza, alta quanto tutto il blocco del ticket.
|
|
111
|
+
const GUTTER_BAR = 0.25
|
|
112
|
+
const RULE_COLOR = '#3b4261'
|
|
113
|
+
// Il tipo di ticket colorato; gli altri tipi restano attenuati.
|
|
114
|
+
const TYPE_COLORS: Readonly<Record<string, string>> = { build: '#7dcfff', decision: '#ff9e64' }
|
|
95
115
|
|
|
96
116
|
const EMPTY: MapPaneData = {
|
|
97
117
|
map: null,
|
|
@@ -132,7 +152,8 @@ export const myTickets = atom({ plugin: 'trailhead', key: 'myTickets' } as const
|
|
|
132
152
|
export const pendingWork = atom({ plugin: 'trailhead', key: 'pendingWork' } as const, null as number | null)
|
|
133
153
|
// Whether that press waits for the running turn to end (cancellable) or was sent to the host.
|
|
134
154
|
export const pendingPhase = atom({ plugin: 'trailhead', key: 'pendingPhase' } as const, null as 'queued' | 'starting' | null)
|
|
135
|
-
// Set once the pane was
|
|
155
|
+
// Set once the auto-open is done: the pane was seen drawn, or the person acted on it (the command, a close by
|
|
156
|
+
// hand, the close button). An unasked open that stays undrawn does not set it; never auto-opened after it is set.
|
|
136
157
|
export const paneAutoOpened = atom({ plugin: 'trailhead', key: 'paneAutoOpened' } as const, false)
|
|
137
158
|
const PENDING = { plugin: 'trailhead', key: 'pendingWork' } as const
|
|
138
159
|
const WORK_COMMAND = 'trailhead:work'
|
|
@@ -213,12 +234,15 @@ async function setPane($: Dollar, next: MapPaneData) {
|
|
|
213
234
|
type GhResult = { ok: true; stdout: string } | { ok: false; reason: string }
|
|
214
235
|
|
|
215
236
|
// Always run in the pinned repo, so the session's cwd moving out of it never changes what is read.
|
|
237
|
+
// gh resolves {owner}/{repo} through git, so it gets the same scrubbed env as a git call.
|
|
216
238
|
async function gh($: Dollar, pin: RepoPin, args: readonly string[]): Promise<GhResult> {
|
|
217
239
|
try {
|
|
218
|
-
const out = await $.process.run(['gh', ...args], { cwd: pin.root, timeoutMs: GH_TIMEOUT_MS })
|
|
240
|
+
const out = await $.process.run(scrubGitEnv(['gh', ...args]), { cwd: pin.root, timeoutMs: GH_TIMEOUT_MS })
|
|
219
241
|
return out.exitCode === 0 ? { ok: true, stdout: out.stdout } : { ok: false, reason: failureReason(out) }
|
|
220
242
|
} catch (error) {
|
|
221
|
-
|
|
243
|
+
// A deleted cwd fails the spawn as ENOENT, the same as a missing binary: look before blaming gh.
|
|
244
|
+
const missingRoot = (await $.fs.exists(pin.root).catch(() => true)) ? undefined : pin.root
|
|
245
|
+
return { ok: false, reason: failureReason({ error, missingRoot }) }
|
|
222
246
|
}
|
|
223
247
|
}
|
|
224
248
|
|
|
@@ -600,6 +624,10 @@ async function restoreAfterClear($: Dollar) {
|
|
|
600
624
|
export const GH_ISSUE_WRITE =
|
|
601
625
|
/\bgh\b[^\n;&|]*\bissue\s+(?:edit|close|create|comment|reopen|delete|transfer)\b|\bgh\b(?=[^\n;&|]*\sapi\s)(?=[^\n;&|]*\brepos\/[^\s;&|]*\/issues\b)(?=[^\n;&|]*(?:\s(?:-X|--method)[\s=]*["']?(?:POST|PATCH|PUT|DELETE)\b|\s(?:-[fF]|--field\b|--raw-field\b|--input\b)))(?![^\n;&|]*\s(?:-X|--method)[\s=]*["']?GET\b)|\btrailhead-issue-body\.js\b/
|
|
602
626
|
|
|
627
|
+
// A tracker write once heredoc bodies are out: a body line is data, never a command (#369). The matcher
|
|
628
|
+
// stays the regex because the host's `command:` takes no function; the handler runs this check first.
|
|
629
|
+
export const isTrackerWrite = (command: string): boolean => GH_ISSUE_WRITE.test(stripHeredocBodies(command))
|
|
630
|
+
|
|
603
631
|
// The host runs the verb in the session's own directory, which a shell `cd` may have moved, and the mod cannot
|
|
604
632
|
// move it back: `out` hands the pinned checkout to the verb, which moves first (#312). A checkout that cannot
|
|
605
633
|
// be read is `unknown`, refused, but not told it is elsewhere.
|
|
@@ -626,7 +654,7 @@ async function placeOf($: Dollar): Promise<{ pin: RepoPin; where: 'in' | 'out' }
|
|
|
626
654
|
// Whether the pinned root still answers as a checkout root, the same test the verb's helper applies.
|
|
627
655
|
async function isCheckoutRoot($: Dollar, root: string): Promise<boolean> {
|
|
628
656
|
try {
|
|
629
|
-
const out = await $.process.run(
|
|
657
|
+
const out = await $.process.run(gitArgv('rev-parse', '--show-toplevel'), { cwd: root, timeoutMs: 5_000 })
|
|
630
658
|
return out.exitCode === 0 && out.stdout.trim() === root
|
|
631
659
|
} catch {
|
|
632
660
|
return false
|
|
@@ -869,12 +897,17 @@ const pressDashboard = ($: Dollar, isOn: boolean): void => {
|
|
|
869
897
|
// so the auto-open runs from a zero-delay timer, a dispatch of its own; `isOpening` keeps two
|
|
870
898
|
// renders from opening twice before the flag is written.
|
|
871
899
|
let isOpening = false
|
|
900
|
+
// Quando l'auto-open ha disegnato il pannello: serve a `/trailhead:panel` per riconoscerne uno mai visto.
|
|
901
|
+
let autoOpenedAt: number | null = null
|
|
872
902
|
// Same for the fill an empty pane asks for from its render.
|
|
873
903
|
let isFilling = false
|
|
874
904
|
|
|
875
|
-
|
|
876
|
-
|
|
905
|
+
// L'auto-open conta come fatto (`paneAutoOpened`) solo quando il pannello è davvero disegnato, o quando la
|
|
906
|
+
// persona è intervenuta (comando, chiusura a mano, bottone close). Un'apertura non richiesta che resta non
|
|
907
|
+
// disegnata non brucia il flag: un trigger successivo (render in fullscreen, `session.attach`) può riprovare.
|
|
908
|
+
async function openPane($: Dollar, isRequested = false) {
|
|
877
909
|
const opened = await $.ui.open({ id: PANE_ID, title: PANE_TITLE })
|
|
910
|
+
if (isRequested || opened.isPlaced) await update($, paneAutoOpened, () => true)
|
|
878
911
|
if (opened.isPlaced) void requestRefresh($)
|
|
879
912
|
return opened
|
|
880
913
|
}
|
|
@@ -883,9 +916,14 @@ async function autoOpen($: Dollar) {
|
|
|
883
916
|
if (isOpening) return
|
|
884
917
|
isOpening = true
|
|
885
918
|
try {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
919
|
+
const pane = (await $.ui.panes()).find(p => p.id === PANE_ID)
|
|
920
|
+
if (pane === undefined) {
|
|
921
|
+
const opened = await openPane($)
|
|
922
|
+
if (opened.isPlaced) autoOpenedAt = await $.clock.now()
|
|
923
|
+
}
|
|
924
|
+
// Found up and drawn (a reload with the pane showing): nothing to open, and renders stop asking.
|
|
925
|
+
else if (pane.isPlaced) await update($, paneAutoOpened, () => true)
|
|
926
|
+
// Listed but undrawn: the host places it itself when it can; its own render then completes the auto-open.
|
|
889
927
|
} catch (err) {
|
|
890
928
|
debug($, `trailhead mod: map pane auto-open failed: ${String(err)}`)
|
|
891
929
|
} finally {
|
|
@@ -893,28 +931,43 @@ async function autoOpen($: Dollar) {
|
|
|
893
931
|
}
|
|
894
932
|
}
|
|
895
933
|
|
|
896
|
-
//
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
934
|
+
// Una lettura sola, da un timer a ritardo zero: un render non può scrivere stato.
|
|
935
|
+
async function autoOpenIfDue($: Dollar) {
|
|
936
|
+
if ((await read($, activeMap)) !== null && !(await read($, paneAutoOpened))) $.clock.after(0, () => void autoOpen($))
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Il render del pannello scatta solo da disegnato: l'auto-open è completato.
|
|
940
|
+
async function markDrawn($: Dollar) {
|
|
941
|
+
if (!(await read($, paneAutoOpened))) $.clock.after(0, () => void update($, paneAutoOpened, () => true).catch(err => debug($, `trailhead mod: auto-open flag not set: ${String(err)}`)))
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Un pannello aperto ma non disegnato (l'auto-open sotto la soglia dei pannelli non richiesti, o su una
|
|
945
|
+
// superficie che non ha ancora comunicato il layout) non è visibile: il comando lo riapre, e una richiesta
|
|
946
|
+
// della persona lo disegna a qualsiasi larghezza. Se nemmeno così viene disegnato NON lo chiude: la persona
|
|
947
|
+
// non l'ha mai visto, chiuderlo risponderebbe "closed" a un pannello mai mostrato. Resta aperto e viene
|
|
948
|
+
// disegnato appena la superficie può collocarlo. Chiude solo un pannello elencato E disegnato. Senza
|
|
949
|
+
// superficie collegata (`no-surface`, l'inizio sessione) la risposta è l'attesa normale.
|
|
901
950
|
const PENDING_MESSAGE = 'Map panel will open as soon as the screen is ready.'
|
|
951
|
+
const NOT_SHOWN_MESSAGE = 'Map panel is open but not shown yet: it appears once the screen can place it.'
|
|
902
952
|
const isNoSurface = (reason: string) => reason.startsWith('no-surface')
|
|
903
953
|
|
|
954
|
+
// Il desktop avvia la sessione al primo invio, non al +: un `/trailhead:panel` come primo messaggio arriva
|
|
955
|
+
// pochi millisecondi dopo l'auto-open. Nessuno fa in tempo a vedere il pannello e a digitare il comando sotto
|
|
956
|
+
// questa soglia, quindi la richiesta è di vederlo, non di chiuderlo.
|
|
957
|
+
const UNSEEN_MS = 2_000
|
|
958
|
+
|
|
904
959
|
async function togglePanel($: Dollar): Promise<string> {
|
|
905
960
|
const pane = (await $.ui.panes()).find(p => p.id === PANE_ID)
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (isNoSurface(reopened.reason)) return PENDING_MESSAGE
|
|
910
|
-
}
|
|
911
|
-
if (pane !== undefined) {
|
|
961
|
+
const isUnseen = autoOpenedAt !== null && (await $.clock.now()) - autoOpenedAt < UNSEEN_MS
|
|
962
|
+
autoOpenedAt = null
|
|
963
|
+
if (pane?.isPlaced === true && !isUnseen) {
|
|
912
964
|
await $.ui.close({ id: PANE_ID })
|
|
913
965
|
return 'Map panel closed.'
|
|
914
966
|
}
|
|
915
|
-
const opened = await openPane(
|
|
967
|
+
const opened = await openPane($, true)
|
|
916
968
|
if (opened.isPlaced) return 'Map panel opened.'
|
|
917
|
-
|
|
969
|
+
debug($, `trailhead mod: map pane not placed: ${opened.reason}`)
|
|
970
|
+
return isNoSurface(opened.reason) ? PENDING_MESSAGE : NOT_SHOWN_MESSAGE
|
|
918
971
|
}
|
|
919
972
|
|
|
920
973
|
// The close button: the pane goes (its `ui.close` hook stops the timers). The press counts as the
|
|
@@ -970,12 +1023,14 @@ export function registerMapPane(on: On) {
|
|
|
970
1023
|
return result
|
|
971
1024
|
})
|
|
972
1025
|
|
|
973
|
-
// After the call ran: a Bash `gh issue` write changed the tracker.
|
|
1026
|
+
// After the call ran: a Bash `gh issue` write changed the tracker (heredoc bodies not counted).
|
|
974
1027
|
on('tool.call', { tool: 'Bash', command: GH_ISSUE_WRITE }, async ($, e, next) => {
|
|
1028
|
+
// The matcher saw the raw text: a write named only inside a heredoc body is no write.
|
|
1029
|
+
if (typeof e.command !== 'string' || !isTrackerWrite(e.command)) return next(e)
|
|
975
1030
|
const result = await next(e)
|
|
976
1031
|
lastWriteAt = await $.clock.now()
|
|
977
1032
|
void requestRefresh($, { isWrite: true })
|
|
978
|
-
await hintWhiteboard($, e.command, result)
|
|
1033
|
+
await hintWhiteboard($, stripHeredocBodies(e.command), result)
|
|
979
1034
|
return result
|
|
980
1035
|
// This hook never judges the call: if it fails, the call goes on untouched.
|
|
981
1036
|
}).catch((_$, e, next) => next(e))
|
|
@@ -986,7 +1041,14 @@ export function registerMapPane(on: On) {
|
|
|
986
1041
|
// Only in fullscreen, where a pane docks beside the transcript; this bare matcher is
|
|
987
1042
|
// narrower than the band's, and the band's own hook (in register.tsx) draws after it.
|
|
988
1043
|
on('ui.render', { component: 'AbovePrompt', viewport: { isFullscreen: true } }, async ($, e, next) => {
|
|
989
|
-
|
|
1044
|
+
await autoOpenIfDue($)
|
|
1045
|
+
return next(e)
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
// A surface that attaches later (a fresh desktop session) may place the pane the first open could not: with
|
|
1049
|
+
// its layout saying fullscreen, the auto-open tries again. Without a viewport the render trigger decides.
|
|
1050
|
+
on('session.attach', { viewport: { isFullscreen: true } }, async ($, e, next) => {
|
|
1051
|
+
await autoOpenIfDue($)
|
|
990
1052
|
return next(e)
|
|
991
1053
|
})
|
|
992
1054
|
|
|
@@ -999,6 +1061,7 @@ export function registerMapPane(on: On) {
|
|
|
999
1061
|
})
|
|
1000
1062
|
|
|
1001
1063
|
on('ui.render', { component: 'Pane', requestId: PANE_ID }, async ($, e) => {
|
|
1064
|
+
await markDrawn($)
|
|
1002
1065
|
const ui = $.ui.resolve(e)
|
|
1003
1066
|
const { Box, Text, Button, Link } = ui
|
|
1004
1067
|
// Raster is the terminal surface's only; elsewhere the constructor is not a real element.
|
|
@@ -1028,14 +1091,45 @@ export function registerMapPane(on: On) {
|
|
|
1028
1091
|
</Box>
|
|
1029
1092
|
)
|
|
1030
1093
|
const link = (url: string, label: string) => (url === '' ? label : <Link href={url} label={label} />)
|
|
1094
|
+
// Il filetto di un blocco (riga e righe sotto): sul desktop una barra che si allunga quanto il blocco, nel
|
|
1095
|
+
// terminale un `│` per riga. Occupa sempre GUTTER colonne.
|
|
1096
|
+
const gutter = (key: string, section: SectionKey, lines: number) => {
|
|
1097
|
+
const { color } = SECTION_STYLES[section]
|
|
1098
|
+
return e.surface === 'desktop' ? (
|
|
1099
|
+
<Box key={key} width={GUTTER.length}>
|
|
1100
|
+
<Box key={`${key}-bar`} width={GUTTER_BAR} backgroundColor={color} />
|
|
1101
|
+
</Box>
|
|
1102
|
+
) : (
|
|
1103
|
+
<Box key={key} flexDirection="column" width={GUTTER.length}>
|
|
1104
|
+
{Array.from({ length: lines }, (_, i) => (
|
|
1105
|
+
<Text key={`${key}-${i}`} color={color}>
|
|
1106
|
+
{GUTTER}
|
|
1107
|
+
</Text>
|
|
1108
|
+
))}
|
|
1109
|
+
</Box>
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
// Intestazione di sezione: glifo e nome nel colore della sezione, poi un filetto fino al bordo.
|
|
1113
|
+
const sectionHead = (key: SectionKey, label: string) => {
|
|
1114
|
+
const { glyph, color } = SECTION_STYLES[key]
|
|
1115
|
+
const rest = cols - glyph.length - 1 - label.length - 1
|
|
1116
|
+
return (
|
|
1117
|
+
<Box key={`section-head-${key}`}>
|
|
1118
|
+
{text(`glyph-${key}`, { color }, `${glyph} `)}
|
|
1119
|
+
{text(`head-${key}`, { bold: true, color }, label)}
|
|
1120
|
+
{rest > 0 ? text(`rule-${key}`, { color: RULE_COLOR }, ` ${'─'.repeat(rest)}`) : null}
|
|
1121
|
+
</Box>
|
|
1122
|
+
)
|
|
1123
|
+
}
|
|
1031
1124
|
|
|
1032
1125
|
// The open maps, one row each; shown with several maps open (or with no active map, to pick one).
|
|
1033
1126
|
// On the dashboard every map is listed and each row opens it.
|
|
1034
1127
|
const mapRow = (m: MapSummary) => {
|
|
1035
|
-
const r = mapRowLayout(m, { active, viewed, columns: cols, isDashboard: isBoard })
|
|
1128
|
+
const r = mapRowLayout(m, { active, viewed, columns: cols - GUTTER.length, isDashboard: isBoard })
|
|
1036
1129
|
return (
|
|
1037
1130
|
<Box key={`map-row-${m.number}`}>
|
|
1038
|
-
{
|
|
1131
|
+
{gutter(`map-gutter-${m.number}`, 'maps', 1)}
|
|
1132
|
+
{text(`map-mark-${m.number}`, r.isActive ? { bold: true, color: SECTION_STYLES.maps.color } : {}, r.mark)}
|
|
1039
1133
|
{r.isPressable ? (
|
|
1040
1134
|
<Button
|
|
1041
1135
|
key={`map-${m.number}`}
|
|
@@ -1052,7 +1146,7 @@ export function registerMapPane(on: On) {
|
|
|
1052
1146
|
const isMapsShown = isBoard ? data.maps.length > 0 : data.maps.length > 1 || (active === null && data.maps.length > 0)
|
|
1053
1147
|
const mapsGroup = isMapsShown ? (
|
|
1054
1148
|
<Box key="group-maps" flexDirection="column" marginTop={gap}>
|
|
1055
|
-
{
|
|
1149
|
+
{sectionHead('maps', `Maps · ${data.maps.length}`)}
|
|
1056
1150
|
{data.maps.map(mapRow)}
|
|
1057
1151
|
</Box>
|
|
1058
1152
|
) : null
|
|
@@ -1091,18 +1185,20 @@ export function registerMapPane(on: On) {
|
|
|
1091
1185
|
|
|
1092
1186
|
// This session's own ticket survives a work-state file another session took over; without one, the file decides.
|
|
1093
1187
|
const isInPlay = (t: PaneTicket) => (own !== null ? t.number === own : play !== null && !orphaned && play.ticket === t.number)
|
|
1094
|
-
const row = (t: PaneTicket, hasMarkColumn: boolean, scope: string | null = null) => {
|
|
1095
|
-
const r = rowLayout(t, { isInPlay: isInPlay(t), hasMarkColumn, pendingWork: pending, pendingPhase: phase, columns: cols })
|
|
1188
|
+
const row = (t: PaneTicket, section: SectionKey, hasMarkColumn: boolean, scope: string | null = null) => {
|
|
1189
|
+
const r = rowLayout(t, { isInPlay: isInPlay(t), hasMarkColumn, pendingWork: pending, pendingPhase: phase, columns: cols - GUTTER.length })
|
|
1096
1190
|
const style = r.isInPlay ? accent : r.isDim ? dim : {}
|
|
1097
1191
|
const holder = r.isDim ? dim : {}
|
|
1192
|
+
const typeColor = t.type === null ? undefined : TYPE_COLORS[t.type]
|
|
1193
|
+
const typeStyle = r.isDim || typeColor === undefined ? dim : { color: typeColor }
|
|
1098
1194
|
const line = (
|
|
1099
|
-
<Box key={`row-${t.number}`} width={cols} justifyContent="space-between">
|
|
1195
|
+
<Box key={`row-${t.number}`} width={cols - GUTTER.length} justifyContent="space-between">
|
|
1100
1196
|
<Box>
|
|
1101
1197
|
{r.mark === null ? null : text(`mark-${t.number}`, r.isInPlay ? accent : {}, r.mark)}
|
|
1102
1198
|
<Box key={`ticket-${t.number}`}>
|
|
1103
1199
|
<Text {...style}>{link(t.url, r.label)}</Text>
|
|
1104
1200
|
</Box>
|
|
1105
|
-
{r.type === null ? null : text(`type-${t.number}`,
|
|
1201
|
+
{r.type === null ? null : text(`type-${t.number}`, typeStyle, r.type)}
|
|
1106
1202
|
</Box>
|
|
1107
1203
|
{r.aside?.kind === 'holder' ? text(`holder-${t.number}`, holder, r.aside.text) : null}
|
|
1108
1204
|
{r.aside?.kind === 'self' ? text(`self-${t.number}`, { color: ACCENT }, r.aside.text) : null}
|
|
@@ -1130,19 +1226,22 @@ export function registerMapPane(on: On) {
|
|
|
1130
1226
|
) : null}
|
|
1131
1227
|
</Box>
|
|
1132
1228
|
)
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
<Box key={`entry-${t.number}`}
|
|
1138
|
-
{
|
|
1139
|
-
{
|
|
1140
|
-
|
|
1229
|
+
// Le righe sotto (genitore UAT, dove sta) rientrano dopo il filetto, che copre tutto il blocco.
|
|
1230
|
+
const parent = uatParentLine(t, cols - GUTTER.length)
|
|
1231
|
+
const lines = 1 + (parent === null ? 0 : 1) + (scope === null ? 0 : 1)
|
|
1232
|
+
return (
|
|
1233
|
+
<Box key={`entry-${t.number}`} width={cols}>
|
|
1234
|
+
{gutter(`gutter-${t.number}`, section, lines)}
|
|
1235
|
+
<Box key={`entry-lines-${t.number}`} flexDirection="column">
|
|
1236
|
+
{line}
|
|
1237
|
+
{parent === null ? null : text(`uat-of-${t.number}`, dim, parent)}
|
|
1238
|
+
{scope === null ? null : text(`scope-of-${t.number}`, dim, scope)}
|
|
1239
|
+
</Box>
|
|
1141
1240
|
</Box>
|
|
1142
1241
|
)
|
|
1143
1242
|
}
|
|
1144
1243
|
// `elsewhere`: my tickets of other scopes, listed after the shown scope's with the line naming where each is.
|
|
1145
|
-
const group = (key:
|
|
1244
|
+
const group = (key: Exclude<SectionKey, 'maps'>, name: string, rows: readonly PaneTicket[], elsewhere: readonly MyTicket[] = []) => {
|
|
1146
1245
|
if (rows.length + elsewhere.length === 0) return null
|
|
1147
1246
|
const hasMarkColumn = [...rows, ...elsewhere].some(isInPlay)
|
|
1148
1247
|
return (
|
|
@@ -1152,9 +1251,9 @@ export function registerMapPane(on: On) {
|
|
|
1152
1251
|
marginTop={gap}
|
|
1153
1252
|
{...(e.surface === 'desktop' ? { rowGap: DESKTOP_ROW_GAP } : {})}
|
|
1154
1253
|
>
|
|
1155
|
-
{
|
|
1156
|
-
{rows.map(t => row(t, hasMarkColumn))}
|
|
1157
|
-
{elsewhere.map(t => row(t, hasMarkColumn, scopeLine(t, data.maps, cols)))}
|
|
1254
|
+
{sectionHead(key, `${name} · ${rows.length + elsewhere.length}`)}
|
|
1255
|
+
{rows.map(t => row(t, key, hasMarkColumn))}
|
|
1256
|
+
{elsewhere.map(t => row(t, key, hasMarkColumn, scopeLine(t, data.maps, cols - GUTTER.length)))}
|
|
1158
1257
|
</Box>
|
|
1159
1258
|
)
|
|
1160
1259
|
}
|
|
@@ -1173,7 +1272,7 @@ export function registerMapPane(on: On) {
|
|
|
1173
1272
|
const notes = (d: { unverifiedCount: number; isCapped: boolean }) => [
|
|
1174
1273
|
d.unverifiedCount === 0 ? null : (
|
|
1175
1274
|
<Box key="unverified" marginTop={gap}>
|
|
1176
|
-
<Text {
|
|
1275
|
+
<Text color={AMBER}>{unverifiedLine(d.unverifiedCount)}</Text>
|
|
1177
1276
|
</Box>
|
|
1178
1277
|
),
|
|
1179
1278
|
!d.isCapped ? null : (
|