@crouton-kit/crouter 0.3.28 → 0.3.29
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/dist/build-root.js +4 -2
- package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +20 -18
- package/dist/builtin-memory/design.md +1 -1
- package/dist/builtin-memory/development.md +1 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/planning.md +1 -1
- package/dist/builtin-memory/spec.md +6 -4
- package/dist/builtin-personas/orchestration-kernel.md +4 -5
- package/dist/builtin-personas/runtime-base.md +5 -4
- package/dist/builtin-personas/spec/PERSONA.md +8 -4
- package/dist/builtin-personas/spec/orchestrator.md +5 -3
- package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
- package/dist/builtin-views/canvas/core.mjs +586 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +165 -0
- package/dist/builtin-views/canvas/web.jsx +120 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +302 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +312 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +427 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.js +3 -3
- package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
- package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
- package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
- package/dist/clients/attach/__tests__/git-info.test.js +31 -0
- package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
- package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +282 -46
- package/dist/clients/attach/auth-pickers.d.ts +9 -0
- package/dist/clients/attach/auth-pickers.js +194 -0
- package/dist/clients/attach/canvas-panels.d.ts +4 -2
- package/dist/clients/attach/canvas-panels.js +45 -17
- package/dist/clients/attach/chat-view.d.ts +27 -2
- package/dist/clients/attach/chat-view.js +98 -9
- package/dist/clients/attach/clipboard-image.d.ts +47 -8
- package/dist/clients/attach/clipboard-image.js +150 -26
- package/dist/clients/attach/clipboard-text.d.ts +3 -0
- package/dist/clients/attach/clipboard-text.js +39 -0
- package/dist/clients/attach/config-load.d.ts +54 -7
- package/dist/clients/attach/config-load.js +99 -3
- package/dist/clients/attach/context-message.d.ts +12 -0
- package/dist/clients/attach/context-message.js +72 -0
- package/dist/clients/attach/git-info.d.ts +20 -0
- package/dist/clients/attach/git-info.js +44 -0
- package/dist/clients/attach/graph-overlay.d.ts +13 -4
- package/dist/clients/attach/graph-overlay.js +59 -19
- package/dist/clients/attach/input-controller.d.ts +75 -13
- package/dist/clients/attach/input-controller.js +305 -67
- package/dist/clients/attach/pickers.d.ts +58 -0
- package/dist/clients/attach/pickers.js +171 -0
- package/dist/clients/attach/slash-commands.d.ts +22 -0
- package/dist/clients/attach/slash-commands.js +91 -25
- package/dist/clients/attach/titled-editor.d.ts +33 -0
- package/dist/clients/attach/titled-editor.js +84 -0
- package/dist/clients/attach/view-socket.d.ts +18 -1
- package/dist/clients/attach/view-socket.js +70 -1
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +59 -0
- package/dist/clients/web/events.d.ts +14 -0
- package/dist/clients/web/events.js +151 -0
- package/dist/clients/web/server.d.ts +18 -0
- package/dist/clients/web/server.js +450 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +120 -0
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
- package/dist/commands/memory/lint.js +14 -2
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/memory/shared.d.ts +7 -1
- package/dist/commands/memory/shared.js +18 -3
- package/dist/commands/memory/write.js +8 -4
- package/dist/commands/memory.js +4 -4
- package/dist/commands/node.d.ts +1 -1
- package/dist/commands/node.js +64 -65
- package/dist/commands/pkg.js +1 -2
- package/dist/commands/revive.js +11 -3
- package/dist/commands/sys/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +3 -2
- package/dist/commands/view-cycle.js +2 -2
- package/dist/commands/view-list.js +8 -8
- package/dist/commands/view-new.js +21 -17
- package/dist/commands/view-pick.js +1 -1
- package/dist/commands/view-run.js +35 -14
- package/dist/commands/view.js +8 -6
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +161 -0
- package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
- package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
- package/dist/core/__tests__/child-death-wake.test.js +56 -111
- package/dist/core/__tests__/close.test.js +14 -20
- package/dist/core/__tests__/context-intro.test.js +19 -19
- package/dist/core/__tests__/daemon-boot.test.js +30 -23
- package/dist/core/__tests__/draw-style.test.js +1 -1
- package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +25 -13
- package/dist/core/__tests__/fork.test.js +22 -5
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
- package/dist/core/__tests__/full/cascade-close.test.js +22 -8
- package/dist/core/__tests__/full/detach-focus.test.js +66 -169
- package/dist/core/__tests__/helpers/harness.d.ts +6 -9
- package/dist/core/__tests__/helpers/harness.js +19 -36
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
- package/dist/core/__tests__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory-resolver.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/reset.test.js +19 -79
- package/dist/core/__tests__/steer-note.test.js +9 -7
- package/dist/core/__tests__/tmux-surface.test.js +13 -7
- package/dist/core/bootstrap.d.ts +0 -3
- package/dist/core/bootstrap.js +1 -143
- package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
- package/dist/core/canvas/browse/app.js +131 -8
- package/dist/core/canvas/browse/model.d.ts +23 -5
- package/dist/core/canvas/browse/model.js +68 -13
- package/dist/core/canvas/browse/render.d.ts +7 -1
- package/dist/core/canvas/browse/render.js +178 -47
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/focuses.d.ts +4 -4
- package/dist/core/canvas/focuses.js +7 -7
- package/dist/core/canvas/nav-model.js +8 -7
- package/dist/core/canvas/render.d.ts +68 -9
- package/dist/core/canvas/render.js +195 -61
- package/dist/core/canvas/types.d.ts +16 -23
- package/dist/core/config.js +1 -4
- package/dist/core/memory-resolver.js +43 -5
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +8 -8
- package/dist/core/runtime/branded-host.d.ts +16 -0
- package/dist/core/runtime/branded-host.js +127 -0
- package/dist/core/runtime/broker-protocol.d.ts +212 -6
- package/dist/core/runtime/broker.d.ts +4 -1
- package/dist/core/runtime/broker.js +494 -58
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +6 -14
- package/dist/core/runtime/host.d.ts +14 -34
- package/dist/core/runtime/host.js +14 -50
- package/dist/core/runtime/launch.d.ts +12 -9
- package/dist/core/runtime/launch.js +27 -15
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +15 -19
- package/dist/core/runtime/nodes.d.ts +0 -55
- package/dist/core/runtime/nodes.js +6 -74
- package/dist/core/runtime/placement.d.ts +91 -335
- package/dist/core/runtime/placement.js +262 -840
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +24 -6
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +107 -0
- package/dist/core/runtime/recycle.js +25 -61
- package/dist/core/runtime/reset.d.ts +43 -37
- package/dist/core/runtime/reset.js +131 -218
- package/dist/core/runtime/revive.d.ts +9 -29
- package/dist/core/runtime/revive.js +40 -139
- package/dist/core/runtime/spawn.d.ts +15 -16
- package/dist/core/runtime/spawn.js +165 -194
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux.d.ts +13 -50
- package/dist/core/runtime/tmux.js +33 -87
- package/dist/core/scope.d.ts +1 -2
- package/dist/core/scope.js +1 -5
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
- package/dist/core/skill-sync/builtins.d.ts +42 -0
- package/dist/core/skill-sync/builtins.js +112 -0
- package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
- package/dist/core/skill-sync/claude-plugins.js +71 -0
- package/dist/core/skill-sync/engine.d.ts +42 -0
- package/dist/core/skill-sync/engine.js +633 -0
- package/dist/core/skill-sync/export.d.ts +23 -0
- package/dist/core/skill-sync/export.js +86 -0
- package/dist/core/skill-sync/manifest.d.ts +64 -0
- package/dist/core/skill-sync/manifest.js +181 -0
- package/dist/core/skill-sync/profile.d.ts +76 -0
- package/dist/core/skill-sync/profile.js +173 -0
- package/dist/core/skill-sync/snapshot.d.ts +57 -0
- package/dist/core/skill-sync/snapshot.js +120 -0
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.js +3 -3
- package/dist/core/substrate/on-read.js +68 -6
- package/dist/core/substrate/render.d.ts +8 -11
- package/dist/core/substrate/render.js +29 -43
- package/dist/core/substrate/schema.d.ts +10 -3
- package/dist/core/substrate/schema.js +6 -3
- package/dist/core/tui/host.d.ts +12 -4
- package/dist/core/tui/host.js +280 -149
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +171 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +70 -0
- package/dist/core/view/transport.d.ts +4 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/daemon/crtrd.d.ts +38 -27
- package/dist/daemon/crtrd.js +303 -376
- package/dist/daemon/manage.js +6 -1
- package/dist/index.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
- package/dist/pi-extensions/canvas-context-intro.js +4 -4
- package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
- package/dist/pi-extensions/canvas-nav.js +5 -0
- package/dist/pi-extensions/canvas-recap.d.ts +37 -0
- package/dist/pi-extensions/canvas-recap.js +236 -0
- package/dist/pi-extensions/canvas-resume.js +2 -1
- package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
- package/dist/pi-extensions/canvas-stophook.js +87 -101
- package/dist/pi-extensions/canvas-view.js +2 -1
- package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
- package/dist/pi-extensions/widget-order-bus.js +34 -0
- package/dist/prompts/view.d.ts +2 -2
- package/dist/prompts/view.js +145 -73
- package/dist/types.d.ts +1 -6
- package/dist/types.js +1 -3
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +16 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +133 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +28 -0
- package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
- package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
- package/dist/web-client/index.html +13 -0
- package/package.json +20 -6
- package/dist/builtin-views/canvas/client.mjs +0 -303
- package/dist/builtin-views/canvas/view.mjs +0 -576
- package/dist/builtin-views/git-pr/client.mjs +0 -440
- package/dist/builtin-views/git-pr/view.mjs +0 -675
- package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
- package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
- package/dist/builtin-views/inbox/view.mjs +0 -889
- package/dist/builtin-views/linkedin/client.mjs +0 -610
- package/dist/builtin-views/linkedin/view.mjs +0 -1171
- package/dist/commands/pkg/bridge.d.ts +0 -1
- package/dist/commands/pkg/bridge.js +0 -137
- package/dist/commands/skill/author.d.ts +0 -3
- package/dist/commands/skill/author.js +0 -140
- package/dist/commands/skill/shared.d.ts +0 -3
- package/dist/commands/skill/shared.js +0 -19
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.js +0 -21
- package/dist/commands/tmux-spread.d.ts +0 -2
- package/dist/commands/tmux-spread.js +0 -144
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
- package/dist/core/__tests__/full/placement-focus.test.js +0 -309
- package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
- package/dist/core/__tests__/full/placement-revive.test.js +0 -238
- package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
- package/dist/core/__tests__/home-session.test.js +0 -186
- package/dist/core/__tests__/relaunch.test.js +0 -335
- package/dist/core/bridge-map.d.ts +0 -19
- package/dist/core/bridge-map.js +0 -73
- package/dist/core/tui/contract.d.ts +0 -83
- package/dist/core/tui/contract.js +0 -8
- package/dist/core/tui/loader.d.ts +0 -16
- package/dist/core/tui/loader.js +0 -94
- package/dist/prompts/skill.d.ts +0 -2
- package/dist/prompts/skill.js +0 -650
- /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
- /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
|
@@ -1,675 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
/**
|
|
3
|
-
* Git / PR board — the crtr `git-pr` view (the monitor archetype, sibling of
|
|
4
|
-
* `canvas`).
|
|
5
|
-
*
|
|
6
|
-
* Self-contained ESM. Imports its data layer from `./client.mjs` (which shells
|
|
7
|
-
* `git` + `gh` over the view's cwd) and the shared state helpers from
|
|
8
|
-
* `../_lib/states.mjs`. It imports NOTHING from crtr — the host injects the
|
|
9
|
-
* `Draw` + `ViewHost` API and dynamically `import()`s this module's DEFAULT
|
|
10
|
-
* EXPORT.
|
|
11
|
-
*
|
|
12
|
-
* A READ-ONLY instrument cluster for the repo at the view's cwd. Composition
|
|
13
|
-
* (single pane — the two domains are stacked, not split, so the eye reads top to
|
|
14
|
-
* bottom without hunting across a rule; a vline is reserved for genuinely
|
|
15
|
-
* co-equal live panes and these aren't — git local state is the headline, PRs
|
|
16
|
-
* are the trailing gauge):
|
|
17
|
-
*
|
|
18
|
-
* 1. HEADER ZONE (always-on gauges, 2 rows): branch → upstream with ahead/
|
|
19
|
-
* behind, a right-flushed working-tree chip; then the last commit + a
|
|
20
|
-
* right-flushed relative age. These are the dials you glance at first.
|
|
21
|
-
* 2. BOARD (the scrollable body, one list with two labeled sections):
|
|
22
|
-
* "Working tree" — changed files (status glyph + XY code + path + churn),
|
|
23
|
-
* then "Pull requests" — open PRs (number + title + review/CI rollup +
|
|
24
|
-
* age). gh failure degrades THIS SECTION to a guided note; the git header +
|
|
25
|
-
* file list still render (graceful partial failure, principle 5).
|
|
26
|
-
*
|
|
27
|
-
* VISUAL LANGUAGE (crtr-views-visual-design §2/§3/§4): hierarchy by weight + hue
|
|
28
|
-
* + position, never boxes. Hues are NUMERIC SGR only — green=clean/passing/
|
|
29
|
-
* staged/ahead (32), yellow=attention/pending/modified/behind (33), red=failing/
|
|
30
|
-
* conflicted (31), cyan=identity/branch/PR# (36), grey=metadata (90), bright-
|
|
31
|
-
* yellow=flag counts (93). Every hue is paired with a glyph + the git-native XY
|
|
32
|
-
* code so it survives NO_COLOR. The one state chip is host-derived from data
|
|
33
|
-
* freshness (busy→working; git source down→error/blocked; conflicts or failing
|
|
34
|
-
* CI / changes-requested→action/attention; else ready), driven by toggling
|
|
35
|
-
* banners + the busy lane. The four standard states come from `_lib/states.mjs`.
|
|
36
|
-
*
|
|
37
|
-
* @module git-pr/view
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import { fetchGit, fetchPrs } from './client.mjs';
|
|
41
|
-
import { loadingState, emptyState, notReadyState } from '../_lib/states.mjs';
|
|
42
|
-
|
|
43
|
-
/** @typedef {import('./client.mjs').GitState} GitState */
|
|
44
|
-
/** @typedef {import('./client.mjs').ChangedFile} ChangedFile */
|
|
45
|
-
/** @typedef {import('./client.mjs').Pr} Pr */
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* One logical board row (built once per refresh; re-rendered on resize without a
|
|
49
|
-
* re-fetch). The cursor moves over all of them — label/spacer rows are inert.
|
|
50
|
-
* @typedef {{kind:'label', text:string}
|
|
51
|
-
* | {kind:'file', file:ChangedFile}
|
|
52
|
-
* | {kind:'clean'}
|
|
53
|
-
* | {kind:'spacer'}
|
|
54
|
-
* | {kind:'pr', pr:Pr}
|
|
55
|
-
* | {kind:'note', text:string}} BoardRow
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The view's single mutable state object. The view owns it; hooks mutate it in
|
|
60
|
-
* place.
|
|
61
|
-
* @typedef {Object} GitPrState
|
|
62
|
-
* @property {GitState|null} git Local git state, or null when unavailable.
|
|
63
|
-
* @property {string|null} gitError Cause string when the git read failed.
|
|
64
|
-
* @property {string|null} gitErrorKind 'not-a-repo' | 'git-missing' | 'git-failed'.
|
|
65
|
-
* @property {Pr[]} prs Open PRs (empty when none / gh degraded).
|
|
66
|
-
* @property {string|null} prNote gh degradation guidance (null ⇒ gh ok).
|
|
67
|
-
* @property {BoardRow[]} board Flattened board rows (render source).
|
|
68
|
-
* @property {number} cursor Read cursor into board (j/k).
|
|
69
|
-
* @property {number} scroll draw.list scroll, stored back each frame.
|
|
70
|
-
* @property {number} lastFetch Epoch ms of the last completed refresh.
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
|
-
// ── Status vocabulary (triple-coded: hue + glyph + the git-native XY code) ─────
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* File class → glyph + NUMERIC hue. Shapes differ (filled/hollow/?/✗) so the
|
|
77
|
-
* class survives mono; the XY code (index vs worktree column) is the third code.
|
|
78
|
-
* @type {Record<string,{glyph:string, fg:string}>}
|
|
79
|
-
*/
|
|
80
|
-
const FILE_GLYPH = {
|
|
81
|
-
staged: { glyph: '●', fg: '32' }, // green — in the index, ready
|
|
82
|
-
modified: { glyph: '○', fg: '33' }, // yellow — unstaged worktree edit
|
|
83
|
-
untracked: { glyph: '?', fg: '90' }, // grey — not tracked
|
|
84
|
-
conflict: { glyph: '✗', fg: '31' }, // red — merge conflict
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/** PR review decision → glyph + hue. Pending review is neutral (grey), not red. */
|
|
88
|
-
const REVIEW = {
|
|
89
|
-
approved: { glyph: '✓', fg: '32' }, // green
|
|
90
|
-
changes: { glyph: '✗', fg: '31' }, // red — changes requested (attention)
|
|
91
|
-
review: { glyph: '◌', fg: '90' }, // grey — review required / pending
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/** PR CI rollup → glyph + hue. */
|
|
95
|
-
const CI = {
|
|
96
|
-
pass: { glyph: '✓', fg: '32' }, // green
|
|
97
|
-
fail: { glyph: '✗', fg: '31' }, // red
|
|
98
|
-
pending: { glyph: '⟳', fg: '33' }, // yellow
|
|
99
|
-
none: { glyph: '·', fg: '90' }, // grey — no checks
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Relative-age ladder (design §5): `now` (<60s), `{m}m` (<60m), `{h}h` (<24h),
|
|
106
|
-
* `{d}d` (<7d), else `Mon D` (`Mar 4`), prior-year `Mon ʼYY`. Max ~5 cols.
|
|
107
|
-
* @param {string} iso @param {number} now @returns {string}
|
|
108
|
-
*/
|
|
109
|
-
function relAge(iso, now) {
|
|
110
|
-
const t = Date.parse(iso);
|
|
111
|
-
if (Number.isNaN(t)) return '';
|
|
112
|
-
const s = Math.max(0, Math.floor((now - t) / 1000));
|
|
113
|
-
if (s < 60) return 'now';
|
|
114
|
-
const m = Math.floor(s / 60);
|
|
115
|
-
if (m < 60) return `${m}m`;
|
|
116
|
-
const h = Math.floor(m / 60);
|
|
117
|
-
if (h < 24) return `${h}h`;
|
|
118
|
-
const d = Math.floor(h / 24);
|
|
119
|
-
if (d < 7) return `${d}d`;
|
|
120
|
-
const date = new Date(t);
|
|
121
|
-
const mon = MONTHS[date.getMonth()] || '?';
|
|
122
|
-
if (date.getFullYear() === new Date(now).getFullYear()) return `${mon} ${date.getDate()}`;
|
|
123
|
-
return `${mon} ʼ${String(date.getFullYear()).slice(-2)}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** @param {number} n @param {string} w @returns {string} */
|
|
127
|
-
function plural(n, w) {
|
|
128
|
-
return `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Total changed files. @param {GitState} g @returns {number} */
|
|
132
|
-
function changedCount(g) {
|
|
133
|
-
return g.files.length;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── Board model (built in refresh; mapped to spans in render) ──────────────────
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Flatten git + PR state into the ordered board rows. Each section degrades
|
|
140
|
-
* inline (a clean reward row / a guided gh note / a "no open PRs" note) so the
|
|
141
|
-
* board is never a void.
|
|
142
|
-
* @param {GitPrState} state @returns {BoardRow[]}
|
|
143
|
-
*/
|
|
144
|
-
function buildBoard(state) {
|
|
145
|
-
/** @type {BoardRow[]} */
|
|
146
|
-
const rows = [];
|
|
147
|
-
const g = state.git;
|
|
148
|
-
rows.push({ kind: 'label', text: 'Working tree' });
|
|
149
|
-
if (!g || g.files.length === 0) rows.push({ kind: 'clean' });
|
|
150
|
-
else for (const f of g.files) rows.push({ kind: 'file', file: f });
|
|
151
|
-
|
|
152
|
-
rows.push({ kind: 'spacer' });
|
|
153
|
-
rows.push({ kind: 'label', text: 'Pull requests' });
|
|
154
|
-
if (state.prNote) rows.push({ kind: 'note', text: state.prNote });
|
|
155
|
-
else if (state.prs.length === 0) rows.push({ kind: 'note', text: 'No open pull requests.' });
|
|
156
|
-
else for (const pr of state.prs) rows.push({ kind: 'pr', pr });
|
|
157
|
-
return rows;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── Chrome copy (subtitle / footer / attention / dump) ─────────────────────────
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Working-tree state as one short phrase, for the subtitle + dump.
|
|
164
|
-
* @param {GitState} g @returns {string}
|
|
165
|
-
*/
|
|
166
|
-
function treePhrase(g) {
|
|
167
|
-
const c = g.counts;
|
|
168
|
-
if (g.files.length === 0) return 'clean';
|
|
169
|
-
/** @type {string[]} */
|
|
170
|
-
const parts = [];
|
|
171
|
-
if (c.conflict) parts.push(`${c.conflict} conflict`);
|
|
172
|
-
if (c.staged) parts.push(`${c.staged} staged`);
|
|
173
|
-
if (c.modified) parts.push(`${c.modified} modified`);
|
|
174
|
-
if (c.untracked) parts.push(`${c.untracked} untracked`);
|
|
175
|
-
return parts.join(' · ') || plural(g.files.length, 'change');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Live title subtitle — branch + tracking delta + tree state. `null` ⇒ no repo.
|
|
180
|
-
* @param {GitPrState} state @returns {string|null}
|
|
181
|
-
*/
|
|
182
|
-
function subtitleFor(state) {
|
|
183
|
-
const g = state.git;
|
|
184
|
-
if (!g) return null;
|
|
185
|
-
let s = g.branch;
|
|
186
|
-
if (g.ahead) s += ` ↑${g.ahead}`;
|
|
187
|
-
if (g.behind) s += ` ↓${g.behind}`;
|
|
188
|
-
s += ` · ${treePhrase(g)}`;
|
|
189
|
-
return s;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Footer status (left, transient) — rendered scope. `null` ⇒ a state speaks.
|
|
194
|
-
* @param {GitPrState} state @returns {string|null}
|
|
195
|
-
*/
|
|
196
|
-
function footerSummary(state) {
|
|
197
|
-
const g = state.git;
|
|
198
|
-
if (!g) return null;
|
|
199
|
-
const files = g.files.length === 0 ? 'clean' : plural(changedCount(g), 'change');
|
|
200
|
-
const prs = state.prNote ? 'PRs n/a' : plural(state.prs.length, 'PR');
|
|
201
|
-
return `${files} · ${prs}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* The one thing that wants a human's eyes → an ACTION banner (host derives the
|
|
206
|
-
* yellow attention chip). Conflicts, a failing PR check, or changes-requested.
|
|
207
|
-
* `null` ⇒ nothing pressing (host derives ready). Only PRs we actually fetched
|
|
208
|
-
* count (gh degraded ⇒ unknown ⇒ not flagged).
|
|
209
|
-
* @param {GitPrState} state @returns {string|null}
|
|
210
|
-
*/
|
|
211
|
-
function attentionFor(state) {
|
|
212
|
-
const g = state.git;
|
|
213
|
-
/** @type {string[]} */
|
|
214
|
-
const parts = [];
|
|
215
|
-
if (g && g.counts.conflict) parts.push(`${plural(g.counts.conflict, 'conflict')}`);
|
|
216
|
-
if (!state.prNote) {
|
|
217
|
-
const failing = state.prs.filter((p) => p.ci === 'fail').length;
|
|
218
|
-
const changes = state.prs.filter((p) => p.review === 'changes').length;
|
|
219
|
-
if (failing) parts.push(`${failing} PR${failing === 1 ? '' : 's'} failing CI`);
|
|
220
|
-
if (changes) parts.push(`${changes} PR${changes === 1 ? '' : 's'} need changes`);
|
|
221
|
-
}
|
|
222
|
-
return parts.length ? `${parts.join(' · ')} — needs attention` : null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ── Row → ListItemRow (left spans + right-flushed metadata) ────────────────────
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Build one board list row. A 1-cell left gutter rides the cursor bg (§2).
|
|
229
|
-
* @param {BoardRow} row @param {number} now
|
|
230
|
-
* @returns {import('../../core/tui/draw.js').ListItemRow}
|
|
231
|
-
*/
|
|
232
|
-
function rowToItem(row, now) {
|
|
233
|
-
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
234
|
-
const spans = [{ text: ' ', style: undefined }];
|
|
235
|
-
|
|
236
|
-
if (row.kind === 'label') {
|
|
237
|
-
spans.push({ text: row.text, style: { fg: '90', dim: true } });
|
|
238
|
-
return { spans };
|
|
239
|
-
}
|
|
240
|
-
if (row.kind === 'spacer') {
|
|
241
|
-
return { spans: [{ text: '', style: undefined }] };
|
|
242
|
-
}
|
|
243
|
-
if (row.kind === 'clean') {
|
|
244
|
-
spans.push({ text: '✓', style: { fg: '32' } });
|
|
245
|
-
spans.push({ text: ' nothing to commit, working tree clean', style: { dim: true } });
|
|
246
|
-
return { spans };
|
|
247
|
-
}
|
|
248
|
-
if (row.kind === 'note') {
|
|
249
|
-
spans.push({ text: '·', style: { fg: '90', dim: true } });
|
|
250
|
-
spans.push({ text: ' ' + row.text, style: { dim: true } });
|
|
251
|
-
return { spans };
|
|
252
|
-
}
|
|
253
|
-
if (row.kind === 'file') {
|
|
254
|
-
const f = row.file;
|
|
255
|
-
const g = FILE_GLYPH[f.cls] || FILE_GLYPH.modified;
|
|
256
|
-
spans.push({ text: g.glyph, style: { fg: g.fg } }); // hue + shape (mono carrier)
|
|
257
|
-
spans.push({ text: ' ', style: undefined });
|
|
258
|
-
spans.push({ text: f.xy.replace(/ /g, '·'), style: { fg: '90', dim: true } }); // git-native XY code
|
|
259
|
-
spans.push({ text: ' ', style: undefined });
|
|
260
|
-
spans.push({ text: f.path, style: f.cls === 'conflict' ? { fg: '31' } : undefined });
|
|
261
|
-
if (f.add || f.del) {
|
|
262
|
-
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
263
|
-
const right = [
|
|
264
|
-
{ text: `+${f.add}`, style: { fg: '90', dim: true } },
|
|
265
|
-
{ text: ' ', style: undefined },
|
|
266
|
-
{ text: `−${f.del}`, style: { fg: '90', dim: true } },
|
|
267
|
-
];
|
|
268
|
-
return { spans, right };
|
|
269
|
-
}
|
|
270
|
-
return { spans };
|
|
271
|
-
}
|
|
272
|
-
// row.kind === 'pr'
|
|
273
|
-
const pr = row.pr;
|
|
274
|
-
const rv = REVIEW[pr.review] || REVIEW.review;
|
|
275
|
-
const ci = CI[pr.ci] || CI.none;
|
|
276
|
-
spans.push({ text: `#${pr.number}`, style: { fg: '36', bold: pr.current } }); // cyan identity; current branch bold
|
|
277
|
-
spans.push({ text: ' ', style: undefined });
|
|
278
|
-
spans.push({ text: pr.title, style: pr.isDraft ? { dim: true } : undefined });
|
|
279
|
-
if (pr.isDraft) spans.push({ text: ' (draft)', style: { fg: '90', dim: true } });
|
|
280
|
-
|
|
281
|
-
const age = relAge(pr.updatedAt, now);
|
|
282
|
-
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
283
|
-
const right = [
|
|
284
|
-
{ text: rv.glyph, style: { fg: rv.fg } }, // review (color + glyph)
|
|
285
|
-
{ text: ' ', style: undefined },
|
|
286
|
-
{ text: ci.glyph, style: { fg: ci.fg } }, // CI rollup (color + glyph)
|
|
287
|
-
];
|
|
288
|
-
if (age) {
|
|
289
|
-
right.push({ text: ' ', style: undefined });
|
|
290
|
-
right.push({ text: age, style: { fg: '90', dim: true } });
|
|
291
|
-
}
|
|
292
|
-
return { spans, right };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ── Header zone (drawn manually above the board list) ──────────────────────────
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Visible (column) width of a span group — ANSI-free cell count. Mirrors
|
|
299
|
-
* draw.ts's internal `spanWidth` so the header can reserve room for a
|
|
300
|
-
* right-flushed group before clipping the left line (Array.from counts code
|
|
301
|
-
* points, so wide/combined glyphs count as one cell each, like the renderer).
|
|
302
|
-
* @param {import('../../core/tui/draw.js').Span[]} spans @returns {number}
|
|
303
|
-
*/
|
|
304
|
-
function spanWidth(spans) {
|
|
305
|
-
let n = 0;
|
|
306
|
-
for (const s of spans) n += Array.from(s.text).length;
|
|
307
|
-
return n;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Paint the always-on header gauges into the top of `content`: branch line then
|
|
312
|
-
* commit line, with right-flushed metadata. Returns the number of rows consumed
|
|
313
|
-
* (incl. the trailing hairline) so render can place the board below it.
|
|
314
|
-
* @param {GitState} g
|
|
315
|
-
* @param {import('../../core/tui/draw.js').Draw} draw
|
|
316
|
-
* @param {import('../../core/tui/draw.js').Rect} content
|
|
317
|
-
* @param {number} now
|
|
318
|
-
* @returns {number} rows consumed
|
|
319
|
-
*/
|
|
320
|
-
function drawHeader(g, draw, content, now) {
|
|
321
|
-
const right = content.col + content.width; // exclusive right edge for spansRight
|
|
322
|
-
let r = content.row;
|
|
323
|
-
|
|
324
|
-
// ── Branch line: ⎇ branch → upstream ↑N ↓M ····· [tree chip right-flush]
|
|
325
|
-
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
326
|
-
const left = [
|
|
327
|
-
{ text: ' ', style: undefined },
|
|
328
|
-
{ text: '⎇ ', style: { fg: '36' } },
|
|
329
|
-
{ text: g.branch, style: { fg: '36', bold: true } }, // cyan identity
|
|
330
|
-
];
|
|
331
|
-
if (g.upstream) {
|
|
332
|
-
left.push({ text: ` → ${g.upstream}`, style: { fg: '90', dim: true } });
|
|
333
|
-
if (g.ahead) left.push({ text: ` ↑${g.ahead}`, style: { fg: '32' } }); // green ahead-ok
|
|
334
|
-
if (g.behind) left.push({ text: ` ↓${g.behind}`, style: { fg: '33' } }); // yellow attention
|
|
335
|
-
if (!g.ahead && !g.behind) left.push({ text: ' ✓ up to date', style: { fg: '32' } });
|
|
336
|
-
} else {
|
|
337
|
-
left.push({ text: ' · no upstream', style: { fg: '90', dim: true } });
|
|
338
|
-
}
|
|
339
|
-
// Tree chip, right-flushed on the same row (clean = green ✓, else colored counts).
|
|
340
|
-
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
341
|
-
const chip = [];
|
|
342
|
-
if (g.files.length === 0) {
|
|
343
|
-
chip.push({ text: '✓ clean', style: { fg: '32' } });
|
|
344
|
-
} else {
|
|
345
|
-
const c = g.counts;
|
|
346
|
-
/** @param {number} n @param {string} word @param {string} glyph @param {string} fg */
|
|
347
|
-
const seg = (n, word, glyph, fg) => {
|
|
348
|
-
if (!n) return;
|
|
349
|
-
if (chip.length) chip.push({ text: ' ', style: undefined });
|
|
350
|
-
chip.push({ text: `${glyph} ${n} ${word}`, style: { fg } });
|
|
351
|
-
};
|
|
352
|
-
seg(c.conflict, 'conflict', '✗', '31');
|
|
353
|
-
seg(c.staged, 'staged', '●', '32');
|
|
354
|
-
seg(c.modified, 'modified', '○', '33');
|
|
355
|
-
seg(c.untracked, 'untracked', '?', '90');
|
|
356
|
-
}
|
|
357
|
-
// Clip the left line so the right-flushed chip never overpaints its tail
|
|
358
|
-
// (mirror draw.list's leftLimit = width − rightWidth − 1).
|
|
359
|
-
const chipW = spanWidth(chip);
|
|
360
|
-
draw.spans(r, content.col, left, chipW ? Math.max(0, content.width - chipW - 1) : content.width);
|
|
361
|
-
draw.spansRight(r, right, chip);
|
|
362
|
-
r++;
|
|
363
|
-
|
|
364
|
-
// ── Commit line: ⊙ sha subject ............................ age (right-flush)
|
|
365
|
-
if (g.lastCommit) {
|
|
366
|
-
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
367
|
-
const commit = [
|
|
368
|
-
{ text: ' ', style: undefined },
|
|
369
|
-
{ text: '⊙ ', style: { fg: '90', dim: true } },
|
|
370
|
-
{ text: g.lastCommit.sha, style: { fg: '33' } }, // yellow sha (git-native)
|
|
371
|
-
{ text: ' ', style: undefined },
|
|
372
|
-
{ text: g.lastCommit.subject, style: undefined },
|
|
373
|
-
];
|
|
374
|
-
// Build the right-flushed age first, then clip the subject so the age never
|
|
375
|
-
// overpaints it (mirror draw.list's leftLimit).
|
|
376
|
-
const age = relAge(g.lastCommit.when, now);
|
|
377
|
-
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
378
|
-
const ageSpans = age ? [{ text: age, style: { fg: '90', dim: true } }] : [];
|
|
379
|
-
const ageW = spanWidth(ageSpans);
|
|
380
|
-
draw.spans(r, content.col, commit, ageW ? Math.max(0, content.width - ageW - 1) : content.width);
|
|
381
|
-
if (age) draw.spansRight(r, right, ageSpans);
|
|
382
|
-
} else {
|
|
383
|
-
draw.spans(r, content.col, [
|
|
384
|
-
{ text: ' ', style: undefined },
|
|
385
|
-
{ text: '⊙ ', style: { fg: '90', dim: true } },
|
|
386
|
-
{ text: 'no commits yet', style: { dim: true } },
|
|
387
|
-
]);
|
|
388
|
-
}
|
|
389
|
-
r++;
|
|
390
|
-
|
|
391
|
-
// Hairline separating the gauges from the board (figure-ground, §3).
|
|
392
|
-
draw.hline(r, content.col, content.col + content.width);
|
|
393
|
-
r++;
|
|
394
|
-
|
|
395
|
-
return r - content.row;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ── Refresh (data lane) ────────────────────────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Read git + PR state, rebuild the board. Runs in the host's single-flight lane.
|
|
402
|
-
* Maps any git failure to guidance (a banner + the freshness chip) and KEEPS the
|
|
403
|
-
* last-known board on a transient git-failed. PR failures degrade the PR section
|
|
404
|
-
* only (an inline note) — the git section always renders.
|
|
405
|
-
* @param {GitPrState} state
|
|
406
|
-
* @param {import('../../core/tui/contract.js').ViewHost} host
|
|
407
|
-
* @returns {Promise<void>}
|
|
408
|
-
*/
|
|
409
|
-
async function refresh(state, host) {
|
|
410
|
-
host.setStatus('Reading git…');
|
|
411
|
-
|
|
412
|
-
const g = await fetchGit();
|
|
413
|
-
if (!g.ok) {
|
|
414
|
-
state.gitError = g.error.message;
|
|
415
|
-
state.gitErrorKind = g.error.kind;
|
|
416
|
-
if (g.error.kind === 'git-failed' && state.git) {
|
|
417
|
-
// Transient — keep the last-known board; raise the cause as a banner.
|
|
418
|
-
host.setError(state.gitError);
|
|
419
|
-
} else {
|
|
420
|
-
// Hard not-ready (no repo / no git binary / first-load failure): drop data
|
|
421
|
-
// so render() shows the guided takeover. The takeover owns the whole rect
|
|
422
|
-
// (design §3) and already names the cause + next step, so DON'T stack a
|
|
423
|
-
// banner under it — clear any stale one. (Soft/partial cases — a transient
|
|
424
|
-
// git-failed that keeps the board, or gh-down — keep their banner below.)
|
|
425
|
-
state.git = null;
|
|
426
|
-
state.prs = [];
|
|
427
|
-
state.prNote = null;
|
|
428
|
-
state.board = buildBoard(state);
|
|
429
|
-
host.setError(null);
|
|
430
|
-
}
|
|
431
|
-
host.setStatus(null);
|
|
432
|
-
host.setSubtitle(subtitleFor(state));
|
|
433
|
-
state.lastFetch = Date.now();
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
state.gitError = null;
|
|
437
|
-
state.gitErrorKind = null;
|
|
438
|
-
state.git = g.data;
|
|
439
|
-
|
|
440
|
-
// PRs — best-effort. A typed error becomes the PR section's guided note.
|
|
441
|
-
const p = await fetchPrs(g.data.branch);
|
|
442
|
-
if (p.ok) {
|
|
443
|
-
state.prs = p.data;
|
|
444
|
-
state.prNote = null;
|
|
445
|
-
} else {
|
|
446
|
-
state.prs = [];
|
|
447
|
-
state.prNote = p.error.message;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
state.board = buildBoard(state);
|
|
451
|
-
if (state.cursor >= state.board.length) state.cursor = Math.max(0, state.board.length - 1);
|
|
452
|
-
state.lastFetch = Date.now();
|
|
453
|
-
|
|
454
|
-
host.setSubtitle(subtitleFor(state));
|
|
455
|
-
|
|
456
|
-
// Data-freshness → state chip. Something that needs eyes (conflicts / failing
|
|
457
|
-
// CI / changes-requested) → ACTION banner ⇒ attention (yellow). Else clear ⇒
|
|
458
|
-
// ready (green). (busy→working is automatic.)
|
|
459
|
-
const attn = attentionFor(state);
|
|
460
|
-
if (attn) host.setBanner(attn, 'action');
|
|
461
|
-
else host.setError(null);
|
|
462
|
-
|
|
463
|
-
host.setStatus(footerSummary(state));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// ── ViewModule ─────────────────────────────────────────────────────────────────
|
|
467
|
-
|
|
468
|
-
/** @type {import('../../core/tui/contract.js').ViewModule<GitPrState>} */
|
|
469
|
-
const view = {
|
|
470
|
-
manifest: {
|
|
471
|
-
id: 'git-pr',
|
|
472
|
-
title: 'Git / PR',
|
|
473
|
-
description: 'Local git state + GitHub PR/CI status for the repo at this cwd',
|
|
474
|
-
refreshMs: 5000,
|
|
475
|
-
keymap: [
|
|
476
|
-
{ keys: 'j/k', label: 'move' },
|
|
477
|
-
{ keys: 'g', label: 'refresh' },
|
|
478
|
-
{ keys: 'q', label: 'quit' },
|
|
479
|
-
],
|
|
480
|
-
},
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Cheap + synchronous initial state — NO slow fetch (the host paints a loading
|
|
484
|
-
* frame, then calls refresh()).
|
|
485
|
-
* @returns {GitPrState}
|
|
486
|
-
*/
|
|
487
|
-
init() {
|
|
488
|
-
return {
|
|
489
|
-
git: null,
|
|
490
|
-
gitError: null,
|
|
491
|
-
gitErrorKind: null,
|
|
492
|
-
prs: [],
|
|
493
|
-
prNote: null,
|
|
494
|
-
board: [],
|
|
495
|
-
cursor: 0,
|
|
496
|
-
scroll: 0,
|
|
497
|
-
lastFetch: 0,
|
|
498
|
-
};
|
|
499
|
-
},
|
|
500
|
-
|
|
501
|
-
refresh,
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Paint the header + board, or one of the four standard states. Pure (reads
|
|
505
|
-
* state, calls draw.*); the only write is storing draw.list's adjusted scroll.
|
|
506
|
-
* @param {GitPrState} state
|
|
507
|
-
* @param {import('../../core/tui/draw.js').Draw} draw
|
|
508
|
-
* @param {import('../../core/tui/draw.js').Rect} content
|
|
509
|
-
*/
|
|
510
|
-
render(state, draw, content) {
|
|
511
|
-
if (content.width <= 0 || content.height <= 0) return;
|
|
512
|
-
|
|
513
|
-
// ── Whole-screen takeovers (no git data to anchor a header) ──
|
|
514
|
-
if (!state.git) {
|
|
515
|
-
if (state.gitErrorKind === 'not-a-repo') {
|
|
516
|
-
notReadyState(draw, content, {
|
|
517
|
-
glyph: '⊙',
|
|
518
|
-
glyphFg: '33', // yellow — pairs with the action chip
|
|
519
|
-
headline: 'Not a git repository',
|
|
520
|
-
explanation: 'This view monitors a git repo, and the current directory is not one.',
|
|
521
|
-
nextStep: 'cd into a repository (or run `git init`), then press g.',
|
|
522
|
-
});
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
if (state.gitErrorKind === 'git-missing') {
|
|
526
|
-
notReadyState(draw, content, {
|
|
527
|
-
glyph: '⚠',
|
|
528
|
-
glyphFg: '31', // red — pairs with the blocked chip
|
|
529
|
-
headline: 'git not found',
|
|
530
|
-
explanation: 'crtr could not find the git binary on PATH.',
|
|
531
|
-
nextStep: 'Install git, then press g.',
|
|
532
|
-
});
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
if (state.lastFetch === 0) {
|
|
536
|
-
loadingState(draw, content, { rows: Math.min(5, content.height), label: 'Reading git…' });
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
// git-failed on the first load (nothing to keep) → guided takeover.
|
|
540
|
-
notReadyState(draw, content, {
|
|
541
|
-
glyph: '⚠',
|
|
542
|
-
glyphFg: '31',
|
|
543
|
-
headline: 'Git unavailable',
|
|
544
|
-
explanation: state.gitError || 'A git command failed.',
|
|
545
|
-
nextStep: 'Press g to retry.',
|
|
546
|
-
});
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const now = Date.now();
|
|
551
|
-
const g = state.git;
|
|
552
|
-
|
|
553
|
-
// ── Header gauges (always on when we have git data) ──
|
|
554
|
-
// Need ≥3 rows for the 2-row header + hairline; below that, skip straight to
|
|
555
|
-
// the board so a tiny pane still shows the changes.
|
|
556
|
-
let bodyRow = content.row;
|
|
557
|
-
let bodyHeight = content.height;
|
|
558
|
-
if (content.height >= 4) {
|
|
559
|
-
const used = drawHeader(g, draw, content, now);
|
|
560
|
-
bodyRow = content.row + used + (content.height - used > 2 ? 1 : 0); // 1-row section gap when there's room
|
|
561
|
-
bodyHeight = content.row + content.height - bodyRow;
|
|
562
|
-
}
|
|
563
|
-
if (bodyHeight <= 0) return;
|
|
564
|
-
const bodyRect = { row: bodyRow, col: content.col, width: content.width, height: bodyHeight };
|
|
565
|
-
|
|
566
|
-
// ── Empty reward: clean tree + no open PRs + gh healthy → "All clear" ──
|
|
567
|
-
if (g.files.length === 0 && state.prs.length === 0 && !state.prNote) {
|
|
568
|
-
emptyState(draw, bodyRect, {
|
|
569
|
-
headline: 'All clear',
|
|
570
|
-
secondary: [`${g.branch} — clean working tree, no open PRs.`, 'Press g to refresh.'],
|
|
571
|
-
});
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// ── The board ──
|
|
576
|
-
const items = state.board.map((row) => rowToItem(row, now));
|
|
577
|
-
const res = draw.list(bodyRect, items, state.cursor, state.scroll);
|
|
578
|
-
state.scroll = res.scroll; // store adjusted scroll back (Draw.list contract)
|
|
579
|
-
},
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Read-only navigation: j/k move the cursor, g refreshes, q quits. No async
|
|
583
|
-
* actions — this is a monitor, not a controller.
|
|
584
|
-
* @param {import('../../core/tui/contract.js').ViewKey} k
|
|
585
|
-
* @param {GitPrState} state
|
|
586
|
-
* @returns {import('../../core/tui/contract.js').ViewAction}
|
|
587
|
-
*/
|
|
588
|
-
onKey(k, state) {
|
|
589
|
-
const key = k.key;
|
|
590
|
-
const ch = k.input;
|
|
591
|
-
if (ch === 'q') return { type: 'quit' };
|
|
592
|
-
if (ch === 'g') return { type: 'refresh' };
|
|
593
|
-
if (key.downArrow || ch === 'j') {
|
|
594
|
-
if (state.board.length) state.cursor = Math.min(state.board.length - 1, state.cursor + 1);
|
|
595
|
-
return { type: 'render' };
|
|
596
|
-
}
|
|
597
|
-
if (key.upArrow || ch === 'k') {
|
|
598
|
-
state.cursor = Math.max(0, state.cursor - 1);
|
|
599
|
-
return { type: 'render' };
|
|
600
|
-
}
|
|
601
|
-
return { type: 'none' };
|
|
602
|
-
},
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Plain-text snapshot for the non-TTY / piped path. No ANSI. The host threads
|
|
606
|
-
* its current banner via `ctx` so guidance surfaces without mirroring it.
|
|
607
|
-
* @param {GitPrState} state
|
|
608
|
-
* @param {import('../../core/tui/contract.js').DumpContext} [ctx]
|
|
609
|
-
* @returns {string}
|
|
610
|
-
*/
|
|
611
|
-
dump(state, ctx) {
|
|
612
|
-
/** @type {string[]} */
|
|
613
|
-
const lines = ['Git / PR board'];
|
|
614
|
-
const banner = ctx && ctx.banner ? ctx.banner : null;
|
|
615
|
-
if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
|
|
616
|
-
else if (state.gitError) lines.push('', `[error] ${state.gitError}`);
|
|
617
|
-
|
|
618
|
-
const g = state.git;
|
|
619
|
-
if (!g) {
|
|
620
|
-
lines.push('', state.lastFetch === 0 ? '(reading git…)' : '(git unavailable)');
|
|
621
|
-
return lines.join('\n');
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const now = Date.now();
|
|
625
|
-
|
|
626
|
-
// Header.
|
|
627
|
-
let head = `⎇ ${g.branch}`;
|
|
628
|
-
if (g.upstream) {
|
|
629
|
-
head += ` → ${g.upstream}`;
|
|
630
|
-
if (g.ahead) head += ` ↑${g.ahead}`;
|
|
631
|
-
if (g.behind) head += ` ↓${g.behind}`;
|
|
632
|
-
if (!g.ahead && !g.behind) head += ' (up to date)';
|
|
633
|
-
} else {
|
|
634
|
-
head += ' (no upstream)';
|
|
635
|
-
}
|
|
636
|
-
head += ` · ${treePhrase(g)}`;
|
|
637
|
-
lines.push('', head);
|
|
638
|
-
if (g.lastCommit) {
|
|
639
|
-
lines.push(`⊙ ${g.lastCommit.sha} ${g.lastCommit.subject} (${relAge(g.lastCommit.when, now)})`);
|
|
640
|
-
} else {
|
|
641
|
-
lines.push('⊙ no commits yet');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Working tree.
|
|
645
|
-
lines.push('', 'Working tree');
|
|
646
|
-
if (g.files.length === 0) {
|
|
647
|
-
lines.push(' ✓ nothing to commit, working tree clean');
|
|
648
|
-
} else {
|
|
649
|
-
for (const f of g.files) {
|
|
650
|
-
const churn = f.add || f.del ? ` +${f.add} −${f.del}` : '';
|
|
651
|
-
lines.push(` ${f.xy.replace(/ /g, '·')} ${f.path}${churn} [${f.cls}]`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// Pull requests.
|
|
656
|
-
lines.push('', 'Pull requests');
|
|
657
|
-
if (state.prNote) {
|
|
658
|
-
lines.push(` · ${state.prNote}`);
|
|
659
|
-
} else if (state.prs.length === 0) {
|
|
660
|
-
lines.push(' · No open pull requests.');
|
|
661
|
-
} else {
|
|
662
|
-
for (const pr of state.prs) {
|
|
663
|
-
const mark = pr.current ? '*' : ' ';
|
|
664
|
-
const draft = pr.isDraft ? ' (draft)' : '';
|
|
665
|
-
lines.push(
|
|
666
|
-
` ${mark}#${pr.number} ${pr.title}${draft} [review:${pr.review} ci:${pr.ci}] (${relAge(pr.updatedAt, now)})`
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return lines.join('\n');
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
export default view;
|