@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Git / PR board — the text presenter (`dump`) for the non-TTY / piped path of
|
|
4
|
+
* the `git-pr` view (`crtr view run git-pr | cat`). Node-only, no ANSI. A plain
|
|
5
|
+
* snapshot of the current state; the host threads its current banner via `ctx`.
|
|
6
|
+
*
|
|
7
|
+
* The pure helpers `dump` shares with the TUI (relAge, treePhrase) are imported
|
|
8
|
+
* from `core.mjs`.
|
|
9
|
+
*
|
|
10
|
+
* @module git-pr/text
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { relAge, treePhrase } from './core.mjs';
|
|
14
|
+
|
|
15
|
+
/** @typedef {import('./core.mjs').GitPrState} GitPrState */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Plain-text snapshot for the non-TTY / piped path.
|
|
19
|
+
* @param {GitPrState} state
|
|
20
|
+
* @param {import('../../core/view/contract.js').DumpContext} [ctx]
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function dump(state, ctx) {
|
|
24
|
+
/** @type {string[]} */
|
|
25
|
+
const lines = ['Git / PR board'];
|
|
26
|
+
const banner = ctx && ctx.banner ? ctx.banner : null;
|
|
27
|
+
if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
|
|
28
|
+
else if (state.gitErr) lines.push('', `[${state.gitErr.display.level}] ${state.gitErr.display.explanation}`);
|
|
29
|
+
|
|
30
|
+
const g = state.git;
|
|
31
|
+
if (!g) {
|
|
32
|
+
lines.push('', state.lastFetch === 0 ? '(reading git…)' : '(git unavailable)');
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
|
|
38
|
+
// Header.
|
|
39
|
+
let head = `⎇ ${g.branch}`;
|
|
40
|
+
if (g.upstream) {
|
|
41
|
+
head += ` → ${g.upstream}`;
|
|
42
|
+
if (g.ahead) head += ` ↑${g.ahead}`;
|
|
43
|
+
if (g.behind) head += ` ↓${g.behind}`;
|
|
44
|
+
if (!g.ahead && !g.behind) head += ' (up to date)';
|
|
45
|
+
} else {
|
|
46
|
+
head += ' (no upstream)';
|
|
47
|
+
}
|
|
48
|
+
head += ` · ${treePhrase(g)}`;
|
|
49
|
+
lines.push('', head);
|
|
50
|
+
if (g.lastCommit) {
|
|
51
|
+
lines.push(`⊙ ${g.lastCommit.sha} ${g.lastCommit.subject} (${relAge(g.lastCommit.when, now)})`);
|
|
52
|
+
} else {
|
|
53
|
+
lines.push('⊙ no commits yet');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Working tree.
|
|
57
|
+
lines.push('', 'Working tree');
|
|
58
|
+
if (g.files.length === 0) {
|
|
59
|
+
lines.push(' ✓ nothing to commit, working tree clean');
|
|
60
|
+
} else {
|
|
61
|
+
for (const f of g.files) {
|
|
62
|
+
const churn = f.add || f.del ? ` +${f.add} −${f.del}` : '';
|
|
63
|
+
lines.push(` ${f.xy.replace(/ /g, '·')} ${f.path}${churn} [${f.cls}]`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Pull requests.
|
|
68
|
+
lines.push('', 'Pull requests');
|
|
69
|
+
if (state.prNote) {
|
|
70
|
+
lines.push(` · ${state.prNote}`);
|
|
71
|
+
} else if (state.prs.length === 0) {
|
|
72
|
+
lines.push(' · No open pull requests.');
|
|
73
|
+
} else {
|
|
74
|
+
for (const pr of state.prs) {
|
|
75
|
+
const mark = pr.current ? '*' : ' ';
|
|
76
|
+
const draft = pr.isDraft ? ' (draft)' : '';
|
|
77
|
+
lines.push(
|
|
78
|
+
` ${mark}#${pr.number} ${pr.title}${draft} [review:${pr.review} ci:${pr.ci}] (${relAge(pr.updatedAt, now)})`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Git / PR board — the TUI presenter (`render` + `keymap`) for the `git-pr` view.
|
|
4
|
+
* Node-only (it uses the host's `Draw` API + the `_lib/states.mjs` draw helpers).
|
|
5
|
+
*
|
|
6
|
+
* `render` is a pure read of state; keystrokes map to named intents through
|
|
7
|
+
* `keymap`. All state + data logic lives in `core.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* VISUAL LANGUAGE (crtr-views-visual-design §2/§3/§4): hierarchy by weight + hue
|
|
10
|
+
* + position, never boxes. Hues are NUMERIC SGR only — green=clean/passing/
|
|
11
|
+
* staged/ahead (32), yellow=attention/pending/modified/behind (33), red=failing/
|
|
12
|
+
* conflicted (31), cyan=identity/branch/PR# (36), grey=metadata (90). Every hue
|
|
13
|
+
* is paired with a glyph + the git-native XY code so it survives NO_COLOR.
|
|
14
|
+
*
|
|
15
|
+
* @module git-pr/tui
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { relAge } from './core.mjs';
|
|
19
|
+
import { loadingState, emptyState, notReadyState } from '../_lib/states.mjs';
|
|
20
|
+
|
|
21
|
+
/** @typedef {import('./core.mjs').GitState} GitState */
|
|
22
|
+
/** @typedef {import('./core.mjs').ChangedFile} ChangedFile */
|
|
23
|
+
/** @typedef {import('./core.mjs').Pr} Pr */
|
|
24
|
+
/** @typedef {import('./core.mjs').BoardRow} BoardRow */
|
|
25
|
+
/** @typedef {import('./core.mjs').GitPrState} GitPrState */
|
|
26
|
+
|
|
27
|
+
// ── Status vocabulary (triple-coded: hue + glyph + the git-native XY code) ─────
|
|
28
|
+
|
|
29
|
+
/** @type {Record<string,{glyph:string, fg:string}>} */
|
|
30
|
+
const FILE_GLYPH = {
|
|
31
|
+
staged: { glyph: '●', fg: '32' }, // green — in the index, ready
|
|
32
|
+
modified: { glyph: '○', fg: '33' }, // yellow — unstaged worktree edit
|
|
33
|
+
untracked: { glyph: '?', fg: '90' }, // grey — not tracked
|
|
34
|
+
conflict: { glyph: '✗', fg: '31' }, // red — merge conflict
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** PR review decision → glyph + hue. Pending review is neutral (grey), not red. */
|
|
38
|
+
const REVIEW = {
|
|
39
|
+
approved: { glyph: '✓', fg: '32' }, // green
|
|
40
|
+
changes: { glyph: '✗', fg: '31' }, // red — changes requested (attention)
|
|
41
|
+
review: { glyph: '◌', fg: '90' }, // grey — review required / pending
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** PR CI rollup → glyph + hue. */
|
|
45
|
+
const CI = {
|
|
46
|
+
pass: { glyph: '✓', fg: '32' }, // green
|
|
47
|
+
fail: { glyph: '✗', fg: '31' }, // red
|
|
48
|
+
pending: { glyph: '⟳', fg: '33' }, // yellow
|
|
49
|
+
none: { glyph: '·', fg: '90' }, // grey — no checks
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ── Row → ListItemRow (left spans + right-flushed metadata) ────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build one board list row. A 1-cell left gutter rides the cursor bg (§2).
|
|
56
|
+
* @param {BoardRow} row @param {number} now
|
|
57
|
+
* @returns {import('../../core/tui/draw.js').ListItemRow}
|
|
58
|
+
*/
|
|
59
|
+
function rowToItem(row, now) {
|
|
60
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
61
|
+
const spans = [{ text: ' ', style: undefined }];
|
|
62
|
+
|
|
63
|
+
if (row.kind === 'label') {
|
|
64
|
+
spans.push({ text: row.text, style: { fg: '90', dim: true } });
|
|
65
|
+
return { spans };
|
|
66
|
+
}
|
|
67
|
+
if (row.kind === 'spacer') {
|
|
68
|
+
return { spans: [{ text: '', style: undefined }] };
|
|
69
|
+
}
|
|
70
|
+
if (row.kind === 'clean') {
|
|
71
|
+
spans.push({ text: '✓', style: { fg: '32' } });
|
|
72
|
+
spans.push({ text: ' nothing to commit, working tree clean', style: { dim: true } });
|
|
73
|
+
return { spans };
|
|
74
|
+
}
|
|
75
|
+
if (row.kind === 'note') {
|
|
76
|
+
spans.push({ text: '·', style: { fg: '90', dim: true } });
|
|
77
|
+
spans.push({ text: ' ' + row.text, style: { dim: true } });
|
|
78
|
+
return { spans };
|
|
79
|
+
}
|
|
80
|
+
if (row.kind === 'file') {
|
|
81
|
+
const f = row.file;
|
|
82
|
+
const g = FILE_GLYPH[f.cls] || FILE_GLYPH.modified;
|
|
83
|
+
spans.push({ text: g.glyph, style: { fg: g.fg } }); // hue + shape (mono carrier)
|
|
84
|
+
spans.push({ text: ' ', style: undefined });
|
|
85
|
+
spans.push({ text: f.xy.replace(/ /g, '·'), style: { fg: '90', dim: true } }); // git-native XY code
|
|
86
|
+
spans.push({ text: ' ', style: undefined });
|
|
87
|
+
spans.push({ text: f.path, style: f.cls === 'conflict' ? { fg: '31' } : undefined });
|
|
88
|
+
if (f.add || f.del) {
|
|
89
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
90
|
+
const right = [
|
|
91
|
+
{ text: `+${f.add}`, style: { fg: '90', dim: true } },
|
|
92
|
+
{ text: ' ', style: undefined },
|
|
93
|
+
{ text: `−${f.del}`, style: { fg: '90', dim: true } },
|
|
94
|
+
];
|
|
95
|
+
return { spans, right };
|
|
96
|
+
}
|
|
97
|
+
return { spans };
|
|
98
|
+
}
|
|
99
|
+
// row.kind === 'pr'
|
|
100
|
+
const pr = row.pr;
|
|
101
|
+
const rv = REVIEW[pr.review] || REVIEW.review;
|
|
102
|
+
const ci = CI[pr.ci] || CI.none;
|
|
103
|
+
spans.push({ text: `#${pr.number}`, style: { fg: '36', bold: pr.current } }); // cyan identity; current branch bold
|
|
104
|
+
spans.push({ text: ' ', style: undefined });
|
|
105
|
+
spans.push({ text: pr.title, style: pr.isDraft ? { dim: true } : undefined });
|
|
106
|
+
if (pr.isDraft) spans.push({ text: ' (draft)', style: { fg: '90', dim: true } });
|
|
107
|
+
|
|
108
|
+
const age = relAge(pr.updatedAt, now);
|
|
109
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
110
|
+
const right = [
|
|
111
|
+
{ text: rv.glyph, style: { fg: rv.fg } }, // review (color + glyph)
|
|
112
|
+
{ text: ' ', style: undefined },
|
|
113
|
+
{ text: ci.glyph, style: { fg: ci.fg } }, // CI rollup (color + glyph)
|
|
114
|
+
];
|
|
115
|
+
if (age) {
|
|
116
|
+
right.push({ text: ' ', style: undefined });
|
|
117
|
+
right.push({ text: age, style: { fg: '90', dim: true } });
|
|
118
|
+
}
|
|
119
|
+
return { spans, right };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Header zone (drawn manually above the board list) ──────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Visible (column) width of a span group — ANSI-free cell count. Mirrors
|
|
126
|
+
* draw.ts's internal `spanWidth` so the header can reserve room for a
|
|
127
|
+
* right-flushed group before clipping the left line.
|
|
128
|
+
* @param {import('../../core/tui/draw.js').Span[]} spans @returns {number}
|
|
129
|
+
*/
|
|
130
|
+
function spanWidth(spans) {
|
|
131
|
+
let n = 0;
|
|
132
|
+
for (const s of spans) n += Array.from(s.text).length;
|
|
133
|
+
return n;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Paint the always-on header gauges into the top of `content`: branch line then
|
|
138
|
+
* commit line, with right-flushed metadata. Returns the number of rows consumed
|
|
139
|
+
* (incl. the trailing hairline) so render can place the board below it.
|
|
140
|
+
* @param {GitState} g
|
|
141
|
+
* @param {import('../../core/tui/draw.js').Draw} draw
|
|
142
|
+
* @param {import('../../core/tui/draw.js').Rect} content
|
|
143
|
+
* @param {number} now
|
|
144
|
+
* @returns {number} rows consumed
|
|
145
|
+
*/
|
|
146
|
+
function drawHeader(g, draw, content, now) {
|
|
147
|
+
const right = content.col + content.width; // exclusive right edge for spansRight
|
|
148
|
+
let r = content.row;
|
|
149
|
+
|
|
150
|
+
// ── Branch line: ⎇ branch → upstream ↑N ↓M ····· [tree chip right-flush]
|
|
151
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
152
|
+
const left = [
|
|
153
|
+
{ text: ' ', style: undefined },
|
|
154
|
+
{ text: '⎇ ', style: { fg: '36' } },
|
|
155
|
+
{ text: g.branch, style: { fg: '36', bold: true } }, // cyan identity
|
|
156
|
+
];
|
|
157
|
+
if (g.upstream) {
|
|
158
|
+
left.push({ text: ` → ${g.upstream}`, style: { fg: '90', dim: true } });
|
|
159
|
+
if (g.ahead) left.push({ text: ` ↑${g.ahead}`, style: { fg: '32' } }); // green ahead-ok
|
|
160
|
+
if (g.behind) left.push({ text: ` ↓${g.behind}`, style: { fg: '33' } }); // yellow attention
|
|
161
|
+
if (!g.ahead && !g.behind) left.push({ text: ' ✓ up to date', style: { fg: '32' } });
|
|
162
|
+
} else {
|
|
163
|
+
left.push({ text: ' · no upstream', style: { fg: '90', dim: true } });
|
|
164
|
+
}
|
|
165
|
+
// Tree chip, right-flushed on the same row (clean = green ✓, else colored counts).
|
|
166
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
167
|
+
const chip = [];
|
|
168
|
+
if (g.files.length === 0) {
|
|
169
|
+
chip.push({ text: '✓ clean', style: { fg: '32' } });
|
|
170
|
+
} else {
|
|
171
|
+
const c = g.counts;
|
|
172
|
+
/** @param {number} n @param {string} word @param {string} glyph @param {string} fg */
|
|
173
|
+
const seg = (n, word, glyph, fg) => {
|
|
174
|
+
if (!n) return;
|
|
175
|
+
if (chip.length) chip.push({ text: ' ', style: undefined });
|
|
176
|
+
chip.push({ text: `${glyph} ${n} ${word}`, style: { fg } });
|
|
177
|
+
};
|
|
178
|
+
seg(c.conflict, 'conflict', '✗', '31');
|
|
179
|
+
seg(c.staged, 'staged', '●', '32');
|
|
180
|
+
seg(c.modified, 'modified', '○', '33');
|
|
181
|
+
seg(c.untracked, 'untracked', '?', '90');
|
|
182
|
+
}
|
|
183
|
+
// Clip the left line so the right-flushed chip never overpaints its tail
|
|
184
|
+
// (mirror draw.list's leftLimit = width − rightWidth − 1).
|
|
185
|
+
const chipW = spanWidth(chip);
|
|
186
|
+
draw.spans(r, content.col, left, chipW ? Math.max(0, content.width - chipW - 1) : content.width);
|
|
187
|
+
draw.spansRight(r, right, chip);
|
|
188
|
+
r++;
|
|
189
|
+
|
|
190
|
+
// ── Commit line: ⊙ sha subject ............................ age (right-flush)
|
|
191
|
+
if (g.lastCommit) {
|
|
192
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
193
|
+
const commit = [
|
|
194
|
+
{ text: ' ', style: undefined },
|
|
195
|
+
{ text: '⊙ ', style: { fg: '90', dim: true } },
|
|
196
|
+
{ text: g.lastCommit.sha, style: { fg: '33' } }, // yellow sha (git-native)
|
|
197
|
+
{ text: ' ', style: undefined },
|
|
198
|
+
{ text: g.lastCommit.subject, style: undefined },
|
|
199
|
+
];
|
|
200
|
+
// Build the right-flushed age first, then clip the subject so the age never
|
|
201
|
+
// overpaints it (mirror draw.list's leftLimit).
|
|
202
|
+
const age = relAge(g.lastCommit.when, now);
|
|
203
|
+
/** @type {import('../../core/tui/draw.js').Span[]} */
|
|
204
|
+
const ageSpans = age ? [{ text: age, style: { fg: '90', dim: true } }] : [];
|
|
205
|
+
const ageW = spanWidth(ageSpans);
|
|
206
|
+
draw.spans(r, content.col, commit, ageW ? Math.max(0, content.width - ageW - 1) : content.width);
|
|
207
|
+
if (age) draw.spansRight(r, right, ageSpans);
|
|
208
|
+
} else {
|
|
209
|
+
draw.spans(r, content.col, [
|
|
210
|
+
{ text: ' ', style: undefined },
|
|
211
|
+
{ text: '⊙ ', style: { fg: '90', dim: true } },
|
|
212
|
+
{ text: 'no commits yet', style: { dim: true } },
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
r++;
|
|
216
|
+
|
|
217
|
+
// Hairline separating the gauges from the board (figure-ground, §3).
|
|
218
|
+
draw.hline(r, content.col, content.col + content.width);
|
|
219
|
+
r++;
|
|
220
|
+
|
|
221
|
+
return r - content.row;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── render ─────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Paint the header + board, or one of the four standard states. Pure (reads
|
|
228
|
+
* state, calls draw.*); the only write is storing draw.list's adjusted scroll.
|
|
229
|
+
* @param {GitPrState} state
|
|
230
|
+
* @param {import('../../core/tui/draw.js').Draw} draw
|
|
231
|
+
* @param {import('../../core/tui/draw.js').Rect} content
|
|
232
|
+
*/
|
|
233
|
+
export function render(state, draw, content) {
|
|
234
|
+
if (content.width <= 0 || content.height <= 0) return;
|
|
235
|
+
|
|
236
|
+
// ── Whole-screen takeovers (no git data to anchor a header) ──
|
|
237
|
+
// The copy comes from the typed SourceError's `display` VERBATIM (the contract
|
|
238
|
+
// display/kind split — we never branch on `kind`); only the glyph + hue are a
|
|
239
|
+
// presentation map off `display.level` (action → ⊙ yellow, error → ⚠ red).
|
|
240
|
+
if (!state.git) {
|
|
241
|
+
if (state.gitErr) {
|
|
242
|
+
const d = state.gitErr.display;
|
|
243
|
+
const g = d.level === 'action' ? { glyph: '⊙', fg: '33' } : { glyph: '⚠', fg: '31' };
|
|
244
|
+
notReadyState(draw, content, {
|
|
245
|
+
glyph: g.glyph,
|
|
246
|
+
glyphFg: g.fg,
|
|
247
|
+
headline: d.headline,
|
|
248
|
+
explanation: d.explanation,
|
|
249
|
+
nextStep: d.nextStep,
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// No error yet → first-load loading skeleton.
|
|
254
|
+
loadingState(draw, content, { rows: Math.min(5, content.height), label: 'Reading git…' });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
const g = state.git;
|
|
260
|
+
|
|
261
|
+
// ── Header gauges (always on when we have git data) ──
|
|
262
|
+
// Need ≥3 rows for the 2-row header + hairline; below that, skip straight to
|
|
263
|
+
// the board so a tiny pane still shows the changes.
|
|
264
|
+
let bodyRow = content.row;
|
|
265
|
+
let bodyHeight = content.height;
|
|
266
|
+
if (content.height >= 4) {
|
|
267
|
+
const used = drawHeader(g, draw, content, now);
|
|
268
|
+
bodyRow = content.row + used + (content.height - used > 2 ? 1 : 0); // 1-row section gap when there's room
|
|
269
|
+
bodyHeight = content.row + content.height - bodyRow;
|
|
270
|
+
}
|
|
271
|
+
if (bodyHeight <= 0) return;
|
|
272
|
+
const bodyRect = { row: bodyRow, col: content.col, width: content.width, height: bodyHeight };
|
|
273
|
+
|
|
274
|
+
// ── Empty reward: clean tree + no open PRs + gh healthy → "All clear" ──
|
|
275
|
+
if (g.files.length === 0 && state.prs.length === 0 && !state.prNote) {
|
|
276
|
+
emptyState(draw, bodyRect, {
|
|
277
|
+
headline: 'All clear',
|
|
278
|
+
secondary: [`${g.branch} — clean working tree, no open PRs.`, 'Press g to refresh.'],
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── The board ──
|
|
284
|
+
const items = state.board.map((row) => rowToItem(row, now));
|
|
285
|
+
const res = draw.list(bodyRect, items, state.cursor, state.scroll);
|
|
286
|
+
state.scroll = res.scroll; // store adjusted scroll back (Draw.list contract)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── keymap ───────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Read-only navigation: j/k move the cursor, g refreshes, q quits. No async
|
|
293
|
+
* actions from input — this is a monitor, not a controller. Footer hints come
|
|
294
|
+
* from these bindings' `hint` fields (the single source of truth).
|
|
295
|
+
* @type {import('../../core/view/contract.js').KeyBinding<GitPrState>[]}
|
|
296
|
+
*/
|
|
297
|
+
export const keymap = [
|
|
298
|
+
{ keys: ['j', 'down'], intent: 'cursorDown', hint: { keys: 'j/k', label: 'move' } },
|
|
299
|
+
{ keys: ['k', 'up'], intent: 'cursorUp' },
|
|
300
|
+
{ keys: ['g'], intent: 'refresh', hint: { keys: 'g', label: 'refresh' } },
|
|
301
|
+
{ keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
|
|
302
|
+
];
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Git / PR board — the WEB presenter for the `git-pr` view (React + Tailwind).
|
|
4
|
+
* Browser-only: consumed solely by the web serve path (Vite owns JSX +
|
|
5
|
+
* Tailwind); NEVER Node-imported. The default export is a pure function of
|
|
6
|
+
* `state`; DOM events call `dispatch(intentName, payload?)`.
|
|
7
|
+
*
|
|
8
|
+
* Same logical model as the TUI presenter (`state.board` + `state.cursor`) read
|
|
9
|
+
* from the SAME portable `core.mjs` — zero shared rendering code with `tui.mjs`
|
|
10
|
+
* (the contract's accepted hard fork: `draw.*` has no web analog). The outer
|
|
11
|
+
* chrome (title / status / banner / state chip) is rendered by `<ViewChrome>`,
|
|
12
|
+
* which wraps this component — so do NOT render it here.
|
|
13
|
+
*
|
|
14
|
+
* @module git-pr/web
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { relAge } from './core.mjs';
|
|
18
|
+
import { Loading, NotReady, ErrorState, Empty } from '@crouton-kit/crouter/web';
|
|
19
|
+
|
|
20
|
+
/** @typedef {import('./core.mjs').GitPrState} GitPrState */
|
|
21
|
+
|
|
22
|
+
// ── Status vocabulary (the web analog of tui.mjs's NUMERIC-SGR maps) ──────────
|
|
23
|
+
|
|
24
|
+
const FILE_GLYPH = {
|
|
25
|
+
staged: { glyph: '●', cls: 'text-green-600' },
|
|
26
|
+
modified: { glyph: '○', cls: 'text-amber-600' },
|
|
27
|
+
untracked: { glyph: '?', cls: 'text-slate-400' },
|
|
28
|
+
conflict: { glyph: '✗', cls: 'text-red-600' },
|
|
29
|
+
};
|
|
30
|
+
const REVIEW = {
|
|
31
|
+
approved: { glyph: '✓', cls: 'text-green-600' },
|
|
32
|
+
changes: { glyph: '✗', cls: 'text-red-600' },
|
|
33
|
+
review: { glyph: '◌', cls: 'text-slate-400' },
|
|
34
|
+
};
|
|
35
|
+
const CI = {
|
|
36
|
+
pass: { glyph: '✓', cls: 'text-green-600' },
|
|
37
|
+
fail: { glyph: '✗', cls: 'text-red-600' },
|
|
38
|
+
pending: { glyph: '⟳', cls: 'text-amber-600' },
|
|
39
|
+
none: { glyph: '·', cls: 'text-slate-400' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ── Header gauges (the web analog of tui.mjs's drawHeader) ─────────────────────
|
|
43
|
+
|
|
44
|
+
/** @param {{ g: import('./core.mjs').GitState }} props */
|
|
45
|
+
function Header({ g }) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
return (
|
|
48
|
+
<div className="border-b border-slate-200 pb-2">
|
|
49
|
+
<div className="flex items-baseline gap-2">
|
|
50
|
+
<span className="font-semibold text-cyan-700">⎇ {g.branch}</span>
|
|
51
|
+
{g.upstream ? (
|
|
52
|
+
<span className="text-slate-400">
|
|
53
|
+
→ {g.upstream}
|
|
54
|
+
{g.ahead ? <span className="ml-1 text-green-600">↑{g.ahead}</span> : null}
|
|
55
|
+
{g.behind ? <span className="ml-1 text-amber-600">↓{g.behind}</span> : null}
|
|
56
|
+
{!g.ahead && !g.behind ? <span className="ml-1 text-green-600">✓ up to date</span> : null}
|
|
57
|
+
</span>
|
|
58
|
+
) : (
|
|
59
|
+
<span className="text-slate-400">· no upstream</span>
|
|
60
|
+
)}
|
|
61
|
+
<span className="ml-auto">{treeChip(g)}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="mt-0.5 text-slate-600">
|
|
64
|
+
{g.lastCommit ? (
|
|
65
|
+
<span className="flex items-baseline gap-2">
|
|
66
|
+
<span className="text-slate-400">⊙</span>
|
|
67
|
+
<span className="text-amber-600">{g.lastCommit.sha}</span>
|
|
68
|
+
<span className="truncate">{g.lastCommit.subject}</span>
|
|
69
|
+
<span className="ml-auto shrink-0 text-slate-400">{relAge(g.lastCommit.when, now)}</span>
|
|
70
|
+
</span>
|
|
71
|
+
) : (
|
|
72
|
+
<span className="text-slate-400">⊙ no commits yet</span>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {import('./core.mjs').GitState} g */
|
|
80
|
+
function treeChip(g) {
|
|
81
|
+
if (g.files.length === 0) return <span className="text-green-600">✓ clean</span>;
|
|
82
|
+
const c = g.counts;
|
|
83
|
+
/** @type {Array<{n:number, word:string, glyph:string, cls:string}>} */
|
|
84
|
+
const segs = [
|
|
85
|
+
{ n: c.conflict, word: 'conflict', glyph: '✗', cls: 'text-red-600' },
|
|
86
|
+
{ n: c.staged, word: 'staged', glyph: '●', cls: 'text-green-600' },
|
|
87
|
+
{ n: c.modified, word: 'modified', glyph: '○', cls: 'text-amber-600' },
|
|
88
|
+
{ n: c.untracked, word: 'untracked', glyph: '?', cls: 'text-slate-400' },
|
|
89
|
+
].filter((s) => s.n);
|
|
90
|
+
return (
|
|
91
|
+
<span className="flex gap-3">
|
|
92
|
+
{segs.map((s, i) => (
|
|
93
|
+
<span key={i} className={s.cls}>{s.glyph} {s.n} {s.word}</span>
|
|
94
|
+
))}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Board rows (the web analog of tui.mjs's rowToItem) ─────────────────────────
|
|
100
|
+
|
|
101
|
+
/** @param {{ row: import('./core.mjs').BoardRow, selected: boolean, onClick: () => void }} props */
|
|
102
|
+
function BoardRow({ row, selected, onClick }) {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const base = `flex items-baseline gap-1.5 rounded px-2 py-0.5 ${selected ? 'bg-slate-200' : ''}`;
|
|
105
|
+
|
|
106
|
+
if (row.kind === 'label') {
|
|
107
|
+
return <li className="px-2 pt-2 pb-0.5 text-xs uppercase tracking-wide text-slate-400">{row.text}</li>;
|
|
108
|
+
}
|
|
109
|
+
if (row.kind === 'spacer') {
|
|
110
|
+
return <li className="h-2" aria-hidden />;
|
|
111
|
+
}
|
|
112
|
+
if (row.kind === 'clean') {
|
|
113
|
+
return (
|
|
114
|
+
<li className={base} onClick={onClick}>
|
|
115
|
+
<span className="text-green-600">✓</span>
|
|
116
|
+
<span className="text-slate-400">nothing to commit, working tree clean</span>
|
|
117
|
+
</li>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (row.kind === 'note') {
|
|
121
|
+
return (
|
|
122
|
+
<li className={base} onClick={onClick}>
|
|
123
|
+
<span className="text-slate-400">·</span>
|
|
124
|
+
<span className="text-slate-400">{row.text}</span>
|
|
125
|
+
</li>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (row.kind === 'file') {
|
|
129
|
+
const f = row.file;
|
|
130
|
+
const g = FILE_GLYPH[f.cls] || FILE_GLYPH.modified;
|
|
131
|
+
return (
|
|
132
|
+
<li className={base} onClick={onClick}>
|
|
133
|
+
<span className={g.cls}>{g.glyph}</span>
|
|
134
|
+
<span className="text-slate-400">{f.xy.replace(/ /g, '·')}</span>
|
|
135
|
+
<span className={f.cls === 'conflict' ? 'text-red-600' : ''}>{f.path}</span>
|
|
136
|
+
{f.add || f.del ? (
|
|
137
|
+
<span className="ml-auto shrink-0 text-slate-400">+{f.add} −{f.del}</span>
|
|
138
|
+
) : null}
|
|
139
|
+
</li>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// row.kind === 'pr'
|
|
143
|
+
const pr = row.pr;
|
|
144
|
+
const rv = REVIEW[pr.review] || REVIEW.review;
|
|
145
|
+
const ci = CI[pr.ci] || CI.none;
|
|
146
|
+
const age = relAge(pr.updatedAt, now);
|
|
147
|
+
return (
|
|
148
|
+
<li className={base} onClick={onClick}>
|
|
149
|
+
<span className={`text-cyan-700 ${pr.current ? 'font-bold' : ''}`}>#{pr.number}</span>
|
|
150
|
+
<span className={pr.isDraft ? 'text-slate-400' : ''}>{pr.title}</span>
|
|
151
|
+
{pr.isDraft ? <span className="text-slate-400">(draft)</span> : null}
|
|
152
|
+
<span className="ml-auto flex shrink-0 items-baseline gap-2">
|
|
153
|
+
<span className={rv.cls}>{rv.glyph}</span>
|
|
154
|
+
<span className={ci.cls}>{ci.glyph}</span>
|
|
155
|
+
{age ? <span className="text-slate-400">{age}</span> : null}
|
|
156
|
+
</span>
|
|
157
|
+
</li>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── The view ───────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {import('../../core/view/contract.js').ViewProps<GitPrState>} props
|
|
165
|
+
*/
|
|
166
|
+
export default function GitPr({ state, dispatch }) {
|
|
167
|
+
// ── Whole-view takeovers (no git data to anchor a header). The copy comes
|
|
168
|
+
// from the typed SourceError's `display` VERBATIM (the contract display/kind
|
|
169
|
+
// split — we never branch on `kind`); only the four-state component is a
|
|
170
|
+
// presentation map off `display.level` so the hue matches the TUI (error →
|
|
171
|
+
// red ErrorState, action → amber NotReady). ──
|
|
172
|
+
if (!state.git) {
|
|
173
|
+
if (state.gitErr) {
|
|
174
|
+
const d = state.gitErr.display;
|
|
175
|
+
const Takeover = d.level === 'error' ? ErrorState : NotReady;
|
|
176
|
+
return (
|
|
177
|
+
<Takeover
|
|
178
|
+
headline={d.headline}
|
|
179
|
+
explanation={d.explanation}
|
|
180
|
+
nextStep={d.nextStep}
|
|
181
|
+
onRetry={() => dispatch('refresh')}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
// No error yet → first-load loading state.
|
|
186
|
+
return <Loading label="Reading git…" />;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const g = state.git;
|
|
190
|
+
|
|
191
|
+
// ── Empty reward: clean tree + no open PRs + gh healthy → "All clear". ──
|
|
192
|
+
const allClear = g.files.length === 0 && state.prs.length === 0 && !state.prNote;
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
className="font-mono text-sm outline-none"
|
|
197
|
+
tabIndex={0}
|
|
198
|
+
onKeyDown={(e) => {
|
|
199
|
+
if (e.key === 'j' || e.key === 'ArrowDown') { dispatch('cursorDown'); e.preventDefault(); }
|
|
200
|
+
else if (e.key === 'k' || e.key === 'ArrowUp') { dispatch('cursorUp'); e.preventDefault(); }
|
|
201
|
+
else if (e.key === 'g') { dispatch('refresh'); }
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
<Header g={g} />
|
|
205
|
+
{allClear ? (
|
|
206
|
+
<Empty label={`${g.branch} — clean working tree, no open PRs.`} />
|
|
207
|
+
) : (
|
|
208
|
+
<ul className="mt-1">
|
|
209
|
+
{state.board.map((row, i) => (
|
|
210
|
+
<BoardRow key={i} row={row} selected={i === state.cursor} onClick={() => dispatch('select', i)} />
|
|
211
|
+
))}
|
|
212
|
+
</ul>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared render helpers for the combined `inbox` view.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* standalone `linkedin` view stays untouched (the contract forbids refactoring
|
|
7
|
-
* it). Self-contained ESM, Node-builtins only, imports NOTHING from crtr — the
|
|
5
|
+
* Self-contained ESM, Node-builtins only, imports NOTHING from crtr — the
|
|
8
6
|
* `inbox` view + its source adapters import these RELATIVELY.
|
|
9
7
|
*
|
|
10
8
|
* Discipline (design §2): all hue is NUMERIC SGR codes; color never carries
|