chamba 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/commands/dev.js +11 -11
- package/dist/commands/menu.js +1 -1
- package/dist/commands/settings.js +9 -6
- package/dist/lib/awake-holder.js +3 -0
- package/dist/lib/awake-marker.js +4 -0
- package/dist/lib/awake.js +1 -0
- package/dist/lib/constants.js +1 -1
- package/dist/lib/global-config.js +3 -3
- package/dist/lib/skills.js +2 -2
- package/package.json +1 -1
- package/templates/pane-apps/client/assets/api-Cyoc3E6K.css +1 -0
- package/templates/pane-apps/client/assets/home-CLFnmg7u.css +1 -0
- package/templates/pane-apps/client/assets/home-CrhBkqDQ.js +1 -0
- package/templates/pane-apps/client/assets/reviews-CKk1bus6.js +153 -0
- package/templates/pane-apps/client/assets/reviews-DzHtA35Q.css +1 -0
- package/templates/pane-apps/client/assets/{files-BkmyervY.js → search-DgFnnu-J.js} +1 -1
- package/templates/pane-apps/client/assets/specs-JvdNFtXR.js +13 -0
- package/templates/pane-apps/client/assets/{useNow-BHFKFraS.js → useNow-Ww-Gffnk.js} +1 -1
- package/templates/pane-apps/client/home/index.html +5 -5
- package/templates/pane-apps/client/reviews/index.html +6 -6
- package/templates/pane-apps/client/specs/index.html +4 -4
- package/templates/pane-apps/server/home.mjs +11 -12
- package/templates/pane-apps/server/reviews.mjs +18 -18
- package/templates/pane-apps/server/specs.mjs +4 -4
- package/templates/skills/dx-spec/SKILL.md +4 -16
- package/templates/skills/dx-spec/references/review-guide.md +13 -7
- package/templates/skills/dx-spec/references/spec-guide.md +11 -4
- package/templates/skills/dx-spec/references/stages.md +38 -22
- package/templates/skills/dx-spec-execute/SKILL.md +5 -21
- package/templates/webterm/README.md +11 -8
- package/templates/webterm/awake.js +27 -0
- package/templates/webterm/config.js +10 -0
- package/templates/webterm/public/app/state.js +4 -0
- package/templates/webterm/public/app/tabs.js +37 -3
- package/templates/webterm/public/styles.css +13 -0
- package/templates/webterm/server.js +24 -1
- package/templates/webterm/tools/home.js +25 -3
- package/templates/pane-apps/client/assets/api-DNLbSBpK.css +0 -1
- package/templates/pane-apps/client/assets/home-3SeGzySM.css +0 -1
- package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +0 -1
- package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +0 -153
- package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +0 -1
- package/templates/pane-apps/client/assets/specs-Cabb15F0.js +0 -13
- package/templates/skills/dx-spec/references/principles-template.md +0 -2
- /package/templates/pane-apps/client/assets/{api-BBDf_rzz.js → api-CguEsvNp.js} +0 -0
|
@@ -48,6 +48,7 @@ The page is mission control for the container: the tab bar at the top is every a
|
|
|
48
48
|
Choosing there starts that one session and nothing else: the default stays where the container put it, and `webterm <agent>` in the container is what moves it.
|
|
49
49
|
A name that is not one of the agents starts nothing: the browser names it, and it ends up as a process.
|
|
50
50
|
- **Up to 8 sessions** (`WEBTERM_MAX_SESSIONS`). The limit is memory: each one is a full agent process.
|
|
51
|
+
- **A small sun at the right of the bar** says the host is keeping itself awake while this container is up, so an agent left to work keeps working. It is a sign and not a button: the setting is the machine's, and the chamba menu on the host is where it is changed. Nothing is shown while the machine sleeps as it always did.
|
|
51
52
|
- **Call it a day with the power button** at the right of the bar: it stops the container, and with it every session in it - browser and terminal alike. It always asks first, naming what ends. The container stops itself by signalling PID 1, which only works because chamba gives the keep-alive a TERM trap; a container created before that says so instead of hanging, and points at the host.
|
|
52
53
|
|
|
53
54
|
## What the tabs are telling you
|
|
@@ -92,7 +93,7 @@ A window whose socket dropped asks it too: an answer means the relay is fine, a
|
|
|
92
93
|
## The workspace colour
|
|
93
94
|
|
|
94
95
|
One colour says which window this is from across a screen full of them: the composer border, the active tab's edge, the buttons, and the frame around the browser tab's icon are all of them that one hue.
|
|
95
|
-
The palette button in the bar,
|
|
96
|
+
The palette button in the bar, before the bell, is where it is set.
|
|
96
97
|
It opens a panel of eleven colours with a way back to automatic in the last cell, and one click applies the colour with nothing to confirm.
|
|
97
98
|
|
|
98
99
|
Until someone picks one, the colour comes from the published port, so two containers open side by side are almost never alike.
|
|
@@ -116,8 +117,9 @@ webterm is baked into the chamba image with its dependencies preinstalled, and t
|
|
|
116
117
|
3. In the container, `webterm <agent>` (`claude`, `opencode`, or `codex`) moves which agent new browser sessions start with.
|
|
117
118
|
The agent is always explicit; `webterm` on its own prints usage and the URL, and never picks one for you.
|
|
118
119
|
The choice is written to `~/.chamba-webterm/agent`, and the host reads it back at the next container start - so it holds across restarts.
|
|
119
|
-
chamba bind-mounts that directory from the workspace cache, and three files live in it: `agent`, the resume stamp `resumed-at`, and `color`, which holds the workspace colour.
|
|
120
|
+
chamba bind-mounts that directory from the workspace cache, and three files the server writes live in it: `agent`, the resume stamp `resumed-at`, and `color`, which holds the workspace colour.
|
|
120
121
|
Home's cache directory `.home/` sits beside them, for the same reason: what outlives the container goes here.
|
|
122
|
+
The `awake` marker sits there too, and it is the one file the host writes and the server only reads: a holder on the host keeps it while it holds the machine awake for this container.
|
|
121
123
|
A rebuild replaces the container and leaves all of it where it is.
|
|
122
124
|
|
|
123
125
|
The first session after a container start reopens the most recent conversation; later sessions start fresh.
|
|
@@ -149,8 +151,9 @@ It reads the workspace repository through git and draws the pulse of it, so the
|
|
|
149
151
|
|
|
150
152
|
- **What it serves.** The workspace as a repository, and nothing else. A directory that is no repository is a state Home draws - one plain line - rather than a failure, and so is a repository with no commit yet.
|
|
151
153
|
- **The child process.** git, through the same shared runner Code Reviews uses, as a closed list of read-only operations of Home's own. A branch name never reaches a `--format=` string as text: where a branch is the subject, Home passes its object id.
|
|
152
|
-
- **When it reads.** When its tab is first given a width, and when the user presses Refresh. Nothing watches the repository and nothing polls it, so a commit made in the terminal beside the tab shows on the next opening or the next refresh. The
|
|
153
|
-
- **
|
|
154
|
+
- **When it reads.** When its tab is first given a width, and when the user presses Refresh. Nothing watches the repository and nothing polls it, so a commit made in the terminal beside the tab shows on the next opening or the next refresh. The button that reads again says in its tip how long ago the page read. The one read that happens without the user is the growth card filling in: its points are counted in the background, so while that read says there are points left the page asks it again, and it stops as soon as the sampler is done.
|
|
155
|
+
- **What it counts.** chamba's own directories stay out of every count of lines and of files: the specs root and the reviews root, as the mount names them. Both tools write what they serve - a spec's forms and answers, a review's grouping and its baseline blobs - and counted as code they say a workspace is mostly JSON. A switch in the header row, beside Refresh, puts them back for the whole page at once: the tree card, the growth card, the plant heights in the garden and the lines tile. It moves counts of lines and of files alone. No count of commits moves with it, and neither does the uncommitted changes beside the branch, the branches card or the size of the ignored paths. The state is held in memory, so a reload comes back with chamba's files left out. The two roots reach the module as a parameter, which is why a workspace that keeps either one somewhere else is served the same way, and a workspace with neither gets no switch.
|
|
156
|
+
- **The cache.** Derived numbers alone - a walk of the history, a verdict per branch, and the growth points, one count of files and lines per commit id - kept in `.home/` under the bind-mounted state directory. The module refuses a cache directory inside the workspace, because a file there would show up in the `git status` of every repository Home opens, and a file in the container would die with it.
|
|
154
157
|
- **No door for the agent.** Three routes, each a GET the page calls: the history, the branches and the tree. No verb, no helper on PATH, no framed sentence and no workspace file served raw. Home is the one tool an agent cannot reach at all.
|
|
155
158
|
- **The bee.** A bee crosses the page about once a minute, and a click on it plays one of two short clips and shows a quote. It is one animated image and two sound files, built into the Home client. Its sound is the bell in the bar: the pane hands that preference to every tool frame in the session message, so Home has no mute of its own. Under reduced motion the bee parks beside the header row and stays there.
|
|
156
159
|
|
|
@@ -164,7 +167,7 @@ It reads the specs the workspace holds, and it is where the spec workflow grows.
|
|
|
164
167
|
- **What it serves.** One directory of the workspace, `WEBTERM_SPECS_ROOT`, which is `specs/` unless it is set. The directory is a mount parameter rather than a name inside the tool, so the tool serves whatever a repository calls its specs.
|
|
165
168
|
- **Live.** Every change under that root reaches every open window, not one: a spec belongs to the repository rather than to a session, so two windows reading the same spec both hear it.
|
|
166
169
|
- **In a frame of its own.** The client is not part of this page. It is loaded into an iframe with an opaque origin, and what it may ask the server for is decided by two scoped credentials the shell hands in - see Security below.
|
|
167
|
-
- **What it shows.** The rail on the left, one artifact read as one continuous document in the middle, and, with a spec open, the state of that work on the right. The rail has two modes and never both at once - with nothing open it is what this root holds, the work in hand and the work signed off; with a spec open it is that spec's own artifacts and sections, with the way back at the top. The third pane follows the same rule from the other side: it says what is true of the open spec, so with nothing open there are two panes and the document takes the width. A section carries a read mark, and a section that changed since it was read says so. A page or a diagram beside a spec renders in a frame of the jailed route, an image renders as an image, and anything else is named rather than drawn.
|
|
170
|
+
- **What it shows.** The rail on the left, one artifact read as one continuous document in the middle, and, with a spec open, the state of that work on the right. The rail has two modes and never both at once - with nothing open it is what this root holds, the work in hand under a heading that counts it and, where there is any, the work signed off under a heading of its own; with a spec open it is that spec's own artifacts and sections, with the way back at the top. The work signed off is the half that grows without limit, so its heading says how many and nothing under it is drawn until that heading is pressed. Pressing it also opens a search over those rows, by the name, by the directory, by where the work stands or by the date it was archived, which says "nothing by that name" where none match. Such a row is the name the work calls itself with that date under it, and no status, because everything in the archive carries the same one. The third pane follows the same rule from the other side: it says what is true of the open spec, so with nothing open there are two panes and the document takes the width. A section carries a read mark, and a section that changed since it was read says so. A page or a diagram beside a spec renders in a frame of the jailed route, an image renders as an image, and anything else is named rather than drawn.
|
|
168
171
|
- **Where a link goes.** A link inside a document is never followed by the frame, which has to stay the document the shell put in it. One inside the spec opens in place at the section it named; one out of the repository is asked of the shell, which opens it as a window of its own.
|
|
169
172
|
- **Where a piece of work stands.** One `state.json` inside each spec's `.specs/`, written by the tool alone: the confirmed protocol stage by stage, the mode flags, what the agent is doing right now, what is waiting for the user, and the dated log. `README.md` beside it is rendered from that file every time it moves, so the board in the pane and the file in git are the same facts rather than two copies somebody keeps in step.
|
|
170
173
|
- **Starting one.** `New spec` at the top of the rail is one form - a working name, one box for everything you have to say, and files by drop or paste. The name says what the work will be saved as as you type it, and one quiet line at the foot asks for imagination mode: a wish the agent shapes its recommendation around, and not a decision, because the steps are where the mode is confirmed. Sending it creates the directory, saves what you wrote as `intake.md`, starts the state file, and types the spec command with the fresh directory's name into the terminal - filing the form is the user's own act, so the line is the command the user would type. Each agent spells that command its own way, and the line is spelled for the session it goes to: `/dx-spec` for claude and for opencode, `$dx-spec` for codex. An agent that hears the idea in conversation files the same intake through the helper, and gets the same directory.
|
|
@@ -180,7 +183,7 @@ It reads the specs the workspace holds, and it is where the spec workflow grows.
|
|
|
180
183
|
- **Answering.** A round goes with whatever you answered: a question you left alone is written as unanswered rather than left out, so the agent can tell a "no" from a question you skipped. The first answer wins - two windows can be looking at one gate, and the second is told it was already answered instead of overwriting the first. The outcome becomes a dated line in the state file, the item stops waiting, and one framed line goes to the terminal naming the file the answer went into.
|
|
181
184
|
- **What is waiting for you.** Everything the run is waiting on is a card in the right pane, each naming the artifact it is about and each opening it. They sit at the top of the board, above the protocol, the run and the review rounds: all of those are a record of what has happened, and that record grows for as long as the work runs, so anything under it leaves the pane. The count is on the Specs tab itself, so it is visible from Pages and from the collapsed spine, and it goes when the last item is answered.
|
|
182
185
|
- **The board.** Under the protocol heading, one line per confirmed stage with a mark saying where it stands - green for done, the accent blue for running, muted for waiting. A stage's name is the way into what it produced: one artifact opens from the name itself, several fold out under it on a click and fold away on the next, and a stage that produced nothing is plain text. No list is drawn until it is asked for, because the same paths under stage after stage bury the shape of the work in its own filenames. What the agent is doing at this moment is under its own **Now** heading with a pulsing dot, which is the one line on the board that is not a record of what has happened. It is there while it is current, and the heading goes with it: an activity is over the moment anything else is posted, and one that nothing follows is held for half an hour, because a quiet step is still a step. So a run that ends mid-activity - a container rebuilt, a session closed - takes its line off the board by itself, and the board never says an agent is working when none is. Under **Execution**, once there is an execution plan, the phases and where each one stands; under **Review**, each round of quality review with its verdict and how many findings it made. A pause in the run is a gate like any other: the document explaining it opens, and the gate to carry on stands beside it.
|
|
183
|
-
- **How an agent comes to use it.** The `dx-spec` family of skills, shipped in the image and injected like every other skill: `/dx-spec` builds a spec and `/dx-spec-execute` runs its plan, both honoring the
|
|
186
|
+
- **How an agent comes to use it.** The `dx-spec` family of skills, shipped in the image and injected like every other skill: `/dx-spec` builds a spec and `/dx-spec-execute` runs its plan, both honoring the rules the repository states in its own `AGENTS.md` or `CLAUDE.md`. They carry the craft alone - how to interview, what goes in a spec, how to review it, how to run a plan - and they ask through the tab, because the forms are the product's half. None of them starts a flow in a terminal with no web session: they say how to open one and stop, since there is one flow and it needs the tab.
|
|
184
187
|
|
|
185
188
|
## The Code Reviews tool
|
|
186
189
|
|
|
@@ -245,8 +248,8 @@ A window the account does not have takes its whole meter off the strip rather th
|
|
|
245
248
|
|
|
246
249
|
## Config
|
|
247
250
|
|
|
248
|
-
`config.js` holds the knobs (port, agent list and default agent, key and key file, upload dir, size limit, cleanup age/interval, paste framing, resume stamp, state file, colour file, session limit, keepalive interval, stop timings, claude context file, workspace root and directory-scan limits, the pane's directory, limits, scan interval, feedback bounds and agent stores, the status strip's scan interval, the root and the two scoped credentials each pane tool is mounted with, Home's cache directory, and the bounds on a delivery, per tool).
|
|
249
|
-
Env overrides: `WEBTERM_PORT`, `WEBTERM_CWD`, `WEBTERM_AGENT`, `WEBTERM_AGENT_ARGS`, `WEBTERM_KEY`, `WEBTERM_KEY_FILE`, `WEBTERM_RESUME_STAMP`, `WEBTERM_STATE_FILE`, `WEBTERM_COLOR_FILE`, `WEBTERM_MAX_SESSIONS`, `WEBTERM_PING_INTERVAL_MS`, `WEBTERM_WORKSPACE`, `WEBTERM_CONTEXT_FILE`, `WEBTERM_PANE_DIR`, `WEBTERM_MAX_PAGE_BYTES`, `WEBTERM_MAX_PANE_BYTES`, `WEBTERM_MAX_PANE_FILES`, `WEBTERM_PANE_SCAN_MS`, `WEBTERM_FEEDBACK_MIN_INTERVAL_MS`, `WEBTERM_MAX_FEEDBACK_PER_PAGE`, `WEBTERM_STATUS_SCAN_MS`, `WEBTERM_SUBMIT_DELAY_MS`, `WEBTERM_SPECS_ROOT`, `WEBTERM_SPECS_KEY`, `WEBTERM_SPECS_RAW_TOKEN`, `WEBTERM_MAX_SPECS_LINE_LENGTH`, `WEBTERM_SPECS_LINE_MIN_INTERVAL_MS`, `WEBTERM_MAX_SPECS_LINES_PER_SESSION`, `WEBTERM_SPECS_NEW_SESSION_DELAY_MS`, the same seven for the Code Reviews tool - `WEBTERM_REVIEWS_ROOT`, `WEBTERM_REVIEWS_KEY`, `WEBTERM_REVIEWS_RAW_TOKEN`, `WEBTERM_MAX_REVIEWS_LINE_LENGTH`, `WEBTERM_REVIEWS_LINE_MIN_INTERVAL_MS`, `WEBTERM_MAX_REVIEWS_LINES_PER_SESSION` and `WEBTERM_REVIEWS_NEW_SESSION_DELAY_MS` - `WEBTERM_HOME_CACHE_DIR`, `WEBTERM_HOME_KEY` and `WEBTERM_HOME_RAW_TOKEN`, which are the whole of Home's own set, and the four `WEBTERM_*_DIR` agent-store paths, plus `WEBTERM_PROC_ROOT`, which belongs to `proc.js` rather than to `config.js`.
|
|
251
|
+
`config.js` holds the knobs (port, agent list and default agent, key and key file, upload dir, size limit, cleanup age/interval, paste framing, resume stamp, state file, colour file, the stay-awake marker and how old it may be, session limit, keepalive interval, stop timings, claude context file, workspace root and directory-scan limits, the pane's directory, limits, scan interval, feedback bounds and agent stores, the status strip's scan interval, the root and the two scoped credentials each pane tool is mounted with, Home's cache directory, and the bounds on a delivery, per tool).
|
|
252
|
+
Env overrides: `WEBTERM_PORT`, `WEBTERM_CWD`, `WEBTERM_AGENT`, `WEBTERM_AGENT_ARGS`, `WEBTERM_KEY`, `WEBTERM_KEY_FILE`, `WEBTERM_RESUME_STAMP`, `WEBTERM_STATE_FILE`, `WEBTERM_COLOR_FILE`, `WEBTERM_AWAKE_FILE`, `WEBTERM_AWAKE_STALE_MS`, `WEBTERM_MAX_SESSIONS`, `WEBTERM_PING_INTERVAL_MS`, `WEBTERM_WORKSPACE`, `WEBTERM_CONTEXT_FILE`, `WEBTERM_PANE_DIR`, `WEBTERM_MAX_PAGE_BYTES`, `WEBTERM_MAX_PANE_BYTES`, `WEBTERM_MAX_PANE_FILES`, `WEBTERM_PANE_SCAN_MS`, `WEBTERM_FEEDBACK_MIN_INTERVAL_MS`, `WEBTERM_MAX_FEEDBACK_PER_PAGE`, `WEBTERM_STATUS_SCAN_MS`, `WEBTERM_SUBMIT_DELAY_MS`, `WEBTERM_SPECS_ROOT`, `WEBTERM_SPECS_KEY`, `WEBTERM_SPECS_RAW_TOKEN`, `WEBTERM_MAX_SPECS_LINE_LENGTH`, `WEBTERM_SPECS_LINE_MIN_INTERVAL_MS`, `WEBTERM_MAX_SPECS_LINES_PER_SESSION`, `WEBTERM_SPECS_NEW_SESSION_DELAY_MS`, the same seven for the Code Reviews tool - `WEBTERM_REVIEWS_ROOT`, `WEBTERM_REVIEWS_KEY`, `WEBTERM_REVIEWS_RAW_TOKEN`, `WEBTERM_MAX_REVIEWS_LINE_LENGTH`, `WEBTERM_REVIEWS_LINE_MIN_INTERVAL_MS`, `WEBTERM_MAX_REVIEWS_LINES_PER_SESSION` and `WEBTERM_REVIEWS_NEW_SESSION_DELAY_MS` - `WEBTERM_HOME_CACHE_DIR`, `WEBTERM_HOME_KEY` and `WEBTERM_HOME_RAW_TOKEN`, which are the whole of Home's own set, and the four `WEBTERM_*_DIR` agent-store paths, plus `WEBTERM_PROC_ROOT`, which belongs to `proc.js` rather than to `config.js`.
|
|
250
253
|
`WEBTERM_KEY` pins the key instead of creating one, and each tool's two scoped credentials the same way, which is for tests and hand-run debugging - there is no way to turn any of those gates off.
|
|
251
254
|
`WEBTERM_CWD` is where new sessions start, not where they must stay: the browser can name another directory per session, and `POST /cwd` moves the default.
|
|
252
255
|
`WEBTERM_AGENT` is the same shape: the agent new sessions start with, which the browser can override per session and `POST /agent` moves. A value that is not one of the three falls back to the first, so nothing arbitrary can be spawned through it.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// awake.js - whether the host is being kept awake for this container, as the container can tell.
|
|
2
|
+
//
|
|
3
|
+
// A holder process on the host keeps a marker file in the workspace's state directory while it holds the
|
|
4
|
+
// machine awake, and touches it every half-minute. The container reads that file and nothing else: it cannot
|
|
5
|
+
// see a host process, so freshness is the only honest test it has. A marker older than the trust window is
|
|
6
|
+
// a holder that stopped without taking its file away - a machine that lost power, a process killed outright -
|
|
7
|
+
// and is read as no hold at all.
|
|
8
|
+
//
|
|
9
|
+
// The file is on the host's side of a bind mount, so a write to it raises no event in here and there is
|
|
10
|
+
// nothing to subscribe to. The server looks again on a timer, as it does for the status strip.
|
|
11
|
+
//
|
|
12
|
+
// No globals and nothing done at load, so the tests import this file and run it.
|
|
13
|
+
|
|
14
|
+
import { statSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether the marker says a holder is running now. False for a missing file, an unreadable one, and one whose
|
|
18
|
+
* last touch is older than the window - each of them a hold this container has no reason to believe in.
|
|
19
|
+
*/
|
|
20
|
+
export function isHostAwake(file, staleMs, now = Date.now()) {
|
|
21
|
+
if (!file) return false;
|
|
22
|
+
try {
|
|
23
|
+
return now - statSync(file).mtimeMs < staleMs;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -48,6 +48,16 @@ export const STATE_FILE = process.env.WEBTERM_STATE_FILE || "/home/devuser/.cham
|
|
|
48
48
|
// choice outlives the container: a rebuild replaces everything except what is kept out here.
|
|
49
49
|
export const COLOR_FILE = process.env.WEBTERM_COLOR_FILE || "/home/devuser/.chamba-webterm/color";
|
|
50
50
|
|
|
51
|
+
// The stay-awake marker, and the one file in that directory the host writes and this server only reads: a
|
|
52
|
+
// holder process out there keeps it while it holds the machine awake for this container. Sits beside
|
|
53
|
+
// STATE_FILE in the same mounted directory. Empty disables the indicator.
|
|
54
|
+
export const AWAKE_FILE = process.env.WEBTERM_AWAKE_FILE || "/home/devuser/.chamba-webterm/awake";
|
|
55
|
+
|
|
56
|
+
// How old the marker may be and still be believed. The holder touches it every thirty seconds, so three
|
|
57
|
+
// missed touches is a holder that is not there any more: it cannot be seen from in here, and a machine that
|
|
58
|
+
// lost power leaves a file behind that nothing on this side would ever come back to remove.
|
|
59
|
+
export const AWAKE_STALE_MS = Number(process.env.WEBTERM_AWAKE_STALE_MS) || 90_000;
|
|
60
|
+
|
|
51
61
|
// The mounted workspace, and the only tree a session may be started in. Every directory the interface
|
|
52
62
|
// deals with is this one or something under it.
|
|
53
63
|
export const WORKSPACE_ROOT = "/workspace";
|
|
@@ -17,6 +17,9 @@ export let workspaceName = "";
|
|
|
17
17
|
// the screen: the panel marks this, so "nothing picked" stays tellable from "picked the colour the port
|
|
18
18
|
// would have given anyway".
|
|
19
19
|
export let workspaceColorId = "";
|
|
20
|
+
// Whether the host is holding itself awake for this container. A fact about the machine the container runs
|
|
21
|
+
// on, which the server reads off a file the host writes; nothing in here can see it any other way.
|
|
22
|
+
export let hostAwake = false;
|
|
20
23
|
// Where "+ New session" starts an agent, relative to the workspace root ("" is the root itself). The server
|
|
21
24
|
// owns it - it follows the directory chamba was last run in - and the picker opens on it.
|
|
22
25
|
export let defaultCwd = "";
|
|
@@ -31,6 +34,7 @@ export function adoptSessionsFrame(msg) {
|
|
|
31
34
|
if (Array.isArray(msg.agents)) agents = msg.agents;
|
|
32
35
|
workspaceName = msg.workspace || "";
|
|
33
36
|
workspaceColorId = typeof msg.color === "string" ? msg.color : "";
|
|
37
|
+
hostAwake = msg.awake === true;
|
|
34
38
|
defaultCwd = msg.defaultCwd ?? "";
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -15,7 +15,7 @@ import { closeCard, stopCard } from "./cards.js";
|
|
|
15
15
|
import { sendFrame } from "./connection.js";
|
|
16
16
|
import { SVG_NS, tabbar } from "./dom.js";
|
|
17
17
|
import { repositionNewPop, toggleNewPop } from "./new-session.js";
|
|
18
|
-
import { agents, attachedSid, defaultAgent, defaultCwd, maxSessions, sessions, workspaceName } from "./state.js";
|
|
18
|
+
import { agents, attachedSid, defaultAgent, defaultCwd, hostAwake, maxSessions, sessions, workspaceName } from "./state.js";
|
|
19
19
|
import { focusTerminal, term } from "./terminal.js";
|
|
20
20
|
import { colorFor } from "./theme.js";
|
|
21
21
|
import { repositionColorPop, workspaceColorButton } from "./workspace-color.js";
|
|
@@ -444,6 +444,37 @@ function stopButton() {
|
|
|
444
444
|
return button;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
// The host is holding itself awake for this container. A sign and not a button: there is nothing here for a
|
|
448
|
+
// window to change - the setting is the machine's, and it is changed in the chamba menu on the host.
|
|
449
|
+
function awakeIcon() {
|
|
450
|
+
const sign = document.createElement("span");
|
|
451
|
+
sign.id = "awakeicon";
|
|
452
|
+
sign.title = "The host stays awake while this container is up";
|
|
453
|
+
sign.setAttribute("role", "img");
|
|
454
|
+
sign.setAttribute("aria-label", "The host stays awake while this container is up");
|
|
455
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
456
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
457
|
+
svg.setAttribute("width", "14");
|
|
458
|
+
svg.setAttribute("height", "14");
|
|
459
|
+
svg.setAttribute("fill", "none");
|
|
460
|
+
svg.setAttribute("stroke", "currentColor");
|
|
461
|
+
svg.setAttribute("stroke-width", "2");
|
|
462
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
463
|
+
// A sun: a small circle with eight rays, drawn as one dashed ring outside it so the rays cost one node.
|
|
464
|
+
const core = document.createElementNS(SVG_NS, "circle");
|
|
465
|
+
core.setAttribute("cx", "12");
|
|
466
|
+
core.setAttribute("cy", "12");
|
|
467
|
+
core.setAttribute("r", "4");
|
|
468
|
+
const rays = document.createElementNS(SVG_NS, "circle");
|
|
469
|
+
rays.setAttribute("cx", "12");
|
|
470
|
+
rays.setAttribute("cy", "12");
|
|
471
|
+
rays.setAttribute("r", "9");
|
|
472
|
+
rays.setAttribute("stroke-dasharray", "0.1 6.97");
|
|
473
|
+
svg.append(core, rays);
|
|
474
|
+
sign.append(svg);
|
|
475
|
+
return sign;
|
|
476
|
+
}
|
|
477
|
+
|
|
447
478
|
function chevronIcon() {
|
|
448
479
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
449
480
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
@@ -514,8 +545,11 @@ export function renderBar() {
|
|
|
514
545
|
ws.textContent = workspaceName;
|
|
515
546
|
right.append(ws);
|
|
516
547
|
}
|
|
517
|
-
// The
|
|
518
|
-
//
|
|
548
|
+
// The awake sign first, before the buttons: it is the one thing here nobody presses, and it says
|
|
549
|
+
// something about the machine this container runs on rather than about this window or this session.
|
|
550
|
+
if (hostAwake) right.append(awakeIcon());
|
|
551
|
+
// The colour next: it is about this window's identity rather than about what the window does, so it sits
|
|
552
|
+
// at the near end of the buttons, on the far side of the bell from the one that ends everything.
|
|
519
553
|
right.append(workspaceColorButton(), bellButton(), stopButton());
|
|
520
554
|
tabbar.append(right);
|
|
521
555
|
|
|
@@ -1367,6 +1367,19 @@ body.offline #wscolorpop {
|
|
|
1367
1367
|
pointer-events: none;
|
|
1368
1368
|
}
|
|
1369
1369
|
|
|
1370
|
+
/* ---- The host is being kept awake ---- */
|
|
1371
|
+
|
|
1372
|
+
/* A sign rather than a control: no border, no hover, no pointer. It sits among buttons, so it has to read as
|
|
1373
|
+
the one thing here that nothing happens to when it is pressed. */
|
|
1374
|
+
#awakeicon {
|
|
1375
|
+
display: flex;
|
|
1376
|
+
align-items: center;
|
|
1377
|
+
justify-content: center;
|
|
1378
|
+
padding: 4px;
|
|
1379
|
+
color: var(--muted);
|
|
1380
|
+
opacity: 0.75;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1370
1383
|
/* ---- Sound, and stopping the container ---- */
|
|
1371
1384
|
|
|
1372
1385
|
/* Both sit at the far right of the bar, away from the tabs: neither one is about a single session. */
|
|
@@ -18,11 +18,14 @@ import { join, resolve, sep } from "node:path";
|
|
|
18
18
|
import express from "express";
|
|
19
19
|
import { WebSocketServer } from "ws";
|
|
20
20
|
import { cleanLine, cleanSubmission, createPaneStore, isPageId } from "./artifacts.js";
|
|
21
|
+
import { isHostAwake } from "./awake.js";
|
|
21
22
|
import { readColor, storeColor } from "./color.js";
|
|
22
23
|
import {
|
|
23
24
|
AGENT_ARGS,
|
|
24
25
|
AGENT_STORES,
|
|
25
26
|
AGENTS,
|
|
27
|
+
AWAKE_FILE,
|
|
28
|
+
AWAKE_STALE_MS,
|
|
26
29
|
agentSpawnArgv,
|
|
27
30
|
CHECK_INTERVAL_MS,
|
|
28
31
|
CLIENT_PING_INTERVAL_MS,
|
|
@@ -171,6 +174,10 @@ function sessionAgent(raw) {
|
|
|
171
174
|
// nothing was picked, and the browser works one out from the published port.
|
|
172
175
|
let chosenColor = readColor(COLOR_FILE);
|
|
173
176
|
|
|
177
|
+
// Whether the host is being kept awake for this container. A fact about the machine out there rather than
|
|
178
|
+
// anything this server does, read off the marker on the status timer below and carried on the sessions frame.
|
|
179
|
+
let hostAwake = isHostAwake(AWAKE_FILE, AWAKE_STALE_MS);
|
|
180
|
+
|
|
174
181
|
// The one-line file that names the default agent, for the `webterm` launcher to read back. Written when the
|
|
175
182
|
// port is bound and again whenever the default moves, so a second `webterm <agent>` can say what is live.
|
|
176
183
|
function publishAgent() {
|
|
@@ -501,6 +508,9 @@ function broadcastSessions() {
|
|
|
501
508
|
// The accent colour, on the one frame that already reaches every window: a change has to travel
|
|
502
509
|
// to all of them either way, so this closes the door rather than adding a second one.
|
|
503
510
|
color: chosenColor,
|
|
511
|
+
// The same reasoning as the colour: window-wide, true for every session in here at once, and
|
|
512
|
+
// already travelling to all of them, so it rides this frame rather than opening a second door.
|
|
513
|
+
awake: hostAwake,
|
|
504
514
|
max: MAX_SESSIONS,
|
|
505
515
|
defaultCwd: workspaceLabel(defaultCwd),
|
|
506
516
|
list,
|
|
@@ -691,6 +701,16 @@ function sweepStatus() {
|
|
|
691
701
|
}
|
|
692
702
|
}
|
|
693
703
|
|
|
704
|
+
// The marker read, on the same pass. Nothing in the container hears a write the host made to the mount, so
|
|
705
|
+
// looking again is the whole mechanism - and a frame goes out only when the answer changed, because every
|
|
706
|
+
// window already has the last one.
|
|
707
|
+
function sweepAwake() {
|
|
708
|
+
const awake = isHostAwake(AWAKE_FILE, AWAKE_STALE_MS);
|
|
709
|
+
if (awake === hostAwake) return;
|
|
710
|
+
hostAwake = awake;
|
|
711
|
+
broadcastSessions();
|
|
712
|
+
}
|
|
713
|
+
|
|
694
714
|
// What the host asks before it offers to stop the container. `sessions` is how many conversations are
|
|
695
715
|
// alive (they all die with the container), `attached` how many a window is watching right now, `agent` the
|
|
696
716
|
// one a new session gets and `agents` what is actually running, per agent. Counts and names only - nothing
|
|
@@ -1485,7 +1505,10 @@ setInterval(() => pane.sweep(), PANE_SCAN_MS).unref();
|
|
|
1485
1505
|
|
|
1486
1506
|
// The strip's scan, for the same reason: a snapshot file is written by a shell script into a bind mount, so
|
|
1487
1507
|
// the only way to know it moved is to look.
|
|
1488
|
-
setInterval(() =>
|
|
1508
|
+
setInterval(() => {
|
|
1509
|
+
sweepStatus();
|
|
1510
|
+
sweepAwake();
|
|
1511
|
+
}, STATUS_SCAN_MS).unref();
|
|
1489
1512
|
|
|
1490
1513
|
// Keepalive pass. A window that stops answering is terminated, which releases the session it was
|
|
1491
1514
|
// driving so the window that comes back can pick it up without a takeover prompt. Sessions themselves
|
|
@@ -12,7 +12,16 @@
|
|
|
12
12
|
// =========================================================================================================================================
|
|
13
13
|
|
|
14
14
|
import { createHomeModule, refuseRaw, statusFor } from "../../pane-apps/server/home.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
HOME_CACHE_DIR,
|
|
17
|
+
HOME_KEY,
|
|
18
|
+
HOME_RAW_TOKEN,
|
|
19
|
+
MAX_HOME_BYTES,
|
|
20
|
+
REVIEWS_ROOT,
|
|
21
|
+
SPECS_ROOT,
|
|
22
|
+
WORKSPACE,
|
|
23
|
+
WORKSPACE_ROOT,
|
|
24
|
+
} from "../config.js";
|
|
16
25
|
|
|
17
26
|
// The module maker, re-exported so the path to the built dist is written once, as it is for every tool.
|
|
18
27
|
export { createHomeModule };
|
|
@@ -26,12 +35,25 @@ export const HOME_TABLE = {
|
|
|
26
35
|
statusFor,
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
|
-
* The repository this draws, the directory outside it that the cache goes in,
|
|
38
|
+
* The repository this draws, the directory outside it that the cache goes in, the name to call it, and the two directories the
|
|
39
|
+
* other tools keep their work in.
|
|
30
40
|
*
|
|
31
41
|
* The name is chamba's own for this workspace, which is what the bar wears, so the header row of the page and the corner of the
|
|
32
42
|
* window say the same word. Without it the header would show the mount point, which is "workspace" in every container there is.
|
|
43
|
+
*
|
|
44
|
+
* The two roots are the ones the Specs tool and the Code Reviews tool are mounted with, and Home leaves what is inside them out of
|
|
45
|
+
* every count of lines and of files, with a switch on the page to put them back. Both tools write what they serve - a spec's forms
|
|
46
|
+
* and answers, a review's grouping and its baseline blobs - and counted as code they say a workspace is mostly JSON. They come from
|
|
47
|
+
* here rather than from inside Home for the same reason each tool takes its own root here: a repository that keeps either one
|
|
48
|
+
* elsewhere is served the same way, and Home holds no word about what they are called.
|
|
33
49
|
*/
|
|
34
|
-
create: () =>
|
|
50
|
+
create: () =>
|
|
51
|
+
createHomeModule({
|
|
52
|
+
root: WORKSPACE_ROOT,
|
|
53
|
+
cacheDir: HOME_CACHE_DIR,
|
|
54
|
+
name: WORKSPACE,
|
|
55
|
+
toolPaths: [SPECS_ROOT, REVIEWS_ROOT],
|
|
56
|
+
}),
|
|
35
57
|
|
|
36
58
|
/** Home delivers nothing into a session, so there is no sentence for one to arrive as. */
|
|
37
59
|
events: {},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{--bg:#0d1117;--panel:#161b22;--fg:#e6edf3;--muted:#8b949e;--border:#30363d;--accent:#58a6ff;--ok:#3fb950;--warn:#d29922;--sunken:#0b0e13;--sel:#1f6feb33;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}[data-theme=light]{--bg:#fff;--panel:#f6f8fa;--fg:#1f2328;--muted:#656d76;--border:#d0d7de;--accent:#0969da;--ok:#1a7f37;--warn:#9a6700;--sunken:#f6f8fa;--sel:#ddf4ff;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--fg);margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.6}@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}.workspace{grid-template-columns:minmax(180px,220px) minmax(0,1fr) minmax(220px,280px);height:100%;display:grid}.workspace.is-alone{grid-template-columns:minmax(180px,220px) minmax(0,1fr)}.workspace.has-foot{grid-template-rows:minmax(0,1fr) auto}.workspace-foot{grid-column:1/-1}.nav,.rail{background:var(--panel);font-size:12.5px;overflow:auto}.stage{background:var(--bg);min-width:0;color:var(--fg);flex-direction:column;display:flex;position:relative;overflow:hidden}.stage>*{flex:auto;min-height:0}.nav{border-right:1px solid var(--border);padding:10px 0 24px}.rail{border-left:1px solid var(--border);flex-direction:column;gap:18px;padding:12px 14px 24px;display:flex}.nav-new{border:1px solid var(--border);width:calc(100% - 28px);height:30px;color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:7px;justify-content:center;align-items:center;gap:7px;margin:2px 14px 4px;font-size:12.5px;display:flex}.nav-new:hover{border-color:var(--accent)}.nav-new[aria-pressed=true]{background:var(--sel);border-color:var(--accent)}.nav-new:disabled{color:var(--muted);border-color:var(--border);cursor:default}.nav-note{color:var(--muted);margin:0 14px 4px;font-size:12px;line-height:1.5}.nav-empty{color:var(--muted);margin:0;padding:0 14px}.nav-back{width:100%;color:var(--muted);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:6px;padding:2px 14px 8px;font-size:12px;display:flex}.nav-back:hover{color:var(--accent)}.nav-title{border-bottom:1px solid var(--border);margin-bottom:6px;padding:0 14px 8px}.nav-title-name{overflow-wrap:anywhere;font-weight:600}.nav-title-status{color:var(--muted);letter-spacing:.04em;text-transform:uppercase;font-size:10px}.nav-head,.rail-head{letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:12px 0 5px;padding:0 14px;font-size:10.5px;font-weight:700}.rail-head{padding:0}.nav-head{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.nav-head-count{letter-spacing:0;text-transform:none;font-size:10px;font-weight:400}.nav-head button{all:unset;cursor:pointer;width:100%;color:inherit;font:inherit;letter-spacing:inherit;text-transform:inherit;justify-content:space-between;align-items:baseline;gap:8px;display:flex}.nav-head button:hover{color:var(--fg)}.nav-search{margin:6px 14px 4px}.nav-search input{background:var(--sunken);border:1px solid var(--border);width:100%;color:var(--fg);font:inherit;border-radius:5px;padding:3px 7px;font-size:12px}.nav-title-note{color:var(--muted);font-size:11.5px}.nav.has-foot{flex-direction:column;padding-bottom:0;display:flex}.nav.has-foot>*{flex:none}.nav-foot{border-top:1px solid var(--border);background:var(--panel);color:var(--muted);margin:auto 0 0;padding:8px 14px;font-size:11.5px;position:sticky;bottom:0}.nav-list{--nav-line:1.35;margin:0;padding:0;list-style:none}.nav-list button{text-align:left;width:100%;color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;border-left:2px solid #0000;padding:3px 14px;display:block}.nav-list button:hover{color:var(--accent)}.nav-list button[aria-current]{background:var(--sel);border-left-color:var(--accent)}.nav-list button.nav-artifact{padding-left:22px}.nav-list button.nav-entry{align-items:baseline;gap:6px;display:flex}.nav-entry-name{overflow-wrap:anywhere;line-height:var(--nav-line);flex:1}.nav-archived .nav-entry-name{color:var(--muted)}.nav-entry-status{color:var(--muted);letter-spacing:.04em;text-transform:uppercase;flex:none;font-size:10px}.nav-entry-awaiting{background:var(--accent);min-width:15px;color:var(--bg);text-align:center;border-radius:7px;flex:none;padding:0 4px;font-size:10px;font-weight:700}.nav-dir{color:var(--muted);letter-spacing:.04em;margin:8px 0 2px;padding:0 14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px}.nav-sections{margin:0 0 4px;padding:0;font-size:12px;list-style:none}.nav-sections button{color:var(--muted);padding-left:32px}.nav-sections button[aria-current]{color:var(--fg);font-weight:600}.nav-sections button.is-read{opacity:.55}.nav-changed{color:var(--warn);vertical-align:middle;margin-left:6px;font-size:9px}.doc{padding:20px 40px 96px;position:relative;overflow:auto}.doc-facts{color:var(--muted);flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;display:flex}.doc-changed{color:var(--warn)}.doc-facts-acts{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.doc-section{scroll-margin-top:12px;position:relative}.doc-section.is-read{opacity:.62}.doc-section h2{margin:22px 0 8px;font-size:1.15em}.sec-mark{position:absolute;top:24px;left:-26px}.mark{border:1px solid var(--border);background:var(--bg);color:#0000;cursor:pointer;border-radius:5px;width:20px;height:20px;padding:0;font-size:11px;line-height:1}.doc-section:hover .mark,.mark:focus-visible{color:var(--muted)}.mark[aria-pressed=true]{color:var(--ok);border-color:var(--ok)}.sec-changed{color:var(--warn);margin-left:10px;font-size:11px;font-weight:400}.art-frame{border:1px solid var(--border);background:var(--bg);border-radius:8px;width:100%;height:calc(100vh - 90px)}.art-image img{max-width:100%}.rail-fact{margin:0 0 2px}.theme-toggle{z-index:2;background:var(--bg);color:var(--muted);cursor:pointer;border:none;border-radius:5px;justify-content:center;align-items:center;padding:3px;display:inline-flex;position:absolute;top:20px;right:12px}.theme-toggle svg{display:block}.theme-toggle:hover:enabled{color:var(--accent);background:var(--sel)}.theme-toggle:disabled{opacity:.45;cursor:default}.notice{color:var(--muted);padding:20px 24px}.notice-failure{border-left:3px solid var(--warn);color:var(--fg)}.muted{color:var(--muted)}.visually-hidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.tip{z-index:100;border:1px solid var(--border);background:var(--panel);max-width:min(320px,100vw - 12px);color:var(--fg);text-align:left;white-space:normal;pointer-events:none;border-radius:6px;padding:6px 9px;font-size:12px;font-style:normal;font-weight:400;line-height:1.45;position:fixed;top:0;left:0;box-shadow:0 6px 18px #00000059}.plain{white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;overflow-x:auto}.prose{overflow-wrap:anywhere;font-size:13.5px}.prose h1{margin:18px 0 8px;font-size:1.4em}.prose h2{margin:18px 0 8px;font-size:1.2em}.prose h3{margin:16px 0 6px;font-size:1.08em}.prose h4,.prose h5,.prose h6{color:var(--muted);margin:14px 0 6px;font-size:1em}.prose p{margin:0 0 10px}.prose ul,.prose ol{margin:0 0 10px;padding-left:22px}.prose li{margin:2px 0}.prose li.task{margin-left:-20px;list-style:none}.task-box{border:1px solid var(--muted);vertical-align:baseline;border-radius:3px;width:11px;height:11px;margin-right:8px;display:inline-block}.task-box.is-done{border-color:var(--ok);background:var(--ok)}.prose a{color:var(--accent)}.prose code{background:var(--sunken);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}.prose blockquote{border-left:3px solid var(--border);color:var(--muted);margin:0 0 12px;padding:2px 14px}.prose blockquote>:last-child{margin-bottom:0}.prose hr{border:0;border-top:1px solid var(--border);margin:16px 0}.prose img{max-width:100%}.prose table{border-collapse:collapse;margin:0 0 12px;font-size:.95em;display:block;overflow-x:auto}.prose th,.prose td{border:1px solid var(--border);text-align:left;padding:4px 10px}.prose th{background:var(--sunken)}.prose .cell-left{text-align:left}.prose .cell-center{text-align:center}.prose .cell-right{text-align:right}.prose pre{background:var(--sunken);border:1px solid var(--border);border-radius:8px;margin:0 0 12px;padding:12px;overflow-x:auto}.prose pre code{background:0 0;border:0;padding:0}.annotate{z-index:2;border:1px solid var(--border);background:var(--panel);border-radius:8px;align-items:center;gap:2px;height:30px;padding:0 5px;display:flex;position:absolute;box-shadow:0 4px 14px #00000059}.annotate button{width:24px;height:24px;color:var(--muted);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}.annotate button:hover{background:var(--sel);color:var(--accent)}.annotate .annotate-drop:hover,.annotate button:nth-child(2):hover{color:var(--accent)}.annotate .annotate-close:hover{color:var(--fg)}.annotate-composer{z-index:2;border:1px solid var(--border);background:var(--panel);border-radius:8px;width:240px;padding:8px;position:absolute;box-shadow:0 4px 14px #00000059}.composer-target{color:var(--muted);margin:0 0 6px;font-size:11px;font-style:italic}.annotate-composer textarea{background:var(--bg);width:100%;height:54px;color:var(--fg);border:1px solid var(--border);font:inherit;resize:vertical;border-radius:6px;padding:5px 7px;font-size:12.5px}.composer-acts{justify-content:flex-end;gap:6px;margin-top:6px;display:flex}.composer-add{border:1px solid var(--accent);color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:2px 10px;font-size:11.5px}.composer-add:disabled{border-color:var(--border);color:var(--muted);cursor:default}.composer-cancel{color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;padding:2px 8px;font-size:11.5px}.composer-cancel:hover{border-color:var(--border);color:var(--fg)}.annotate-whole{color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:5px;align-items:center;padding:3px;display:inline-flex}.annotate-whole svg{display:block}.annotate-whole:hover,.annotate-whole:focus-visible{color:var(--accent);background:var(--sel)}.queue{margin:0 0 8px;padding:0;list-style:none}.filed{margin:0;padding:0;list-style:none}.rail-awaiting{gap:6px;margin:10px 0 0;padding:0;list-style:none;display:grid}.rail-annotations .filed{margin-top:10px}.filed li{border-bottom:1px solid var(--border);align-items:baseline;gap:6px;padding:3px 0;font-size:11.5px;display:flex}.filed li:last-child{border-bottom:none}.filed-round{flex:none;font-weight:600}.filed-count{color:var(--muted);flex:1}.filed-when{color:var(--muted);flex:none;font-size:10.5px}.queued{border:1px solid var(--border);border-left:3px solid var(--accent);background:var(--bg);border-radius:7px;margin-bottom:7px;padding:7px 9px}.queued.is-remove{border-left-color:var(--warn)}.queued-target{color:var(--muted);margin:0;font-size:11px;font-style:italic}.queued-kind{color:var(--warn);margin:3px 0 0;font-size:11.5px}.queued-text{margin:3px 0 0;font-size:12.5px}.queued-empty{color:var(--muted);margin:3px 0 0;font-size:12.5px;font-style:italic}.queued textarea{background:var(--panel);width:100%;min-height:42px;color:inherit;border:1px solid var(--border);font:inherit;resize:vertical;border-radius:6px;margin-top:4px;padding:5px 7px;font-size:12px}.queued-acts{gap:10px;margin-top:4px;display:flex}.queued-acts button{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:none;padding:0;font-size:11px}.queued-acts button:disabled{color:var(--muted);cursor:default}.send{background:var(--accent);width:100%;color:var(--bg);font:inherit;cursor:pointer;border:none;border-radius:7px;padding:6px 0;font-size:12px}.send:disabled{background:var(--border);color:var(--muted);cursor:default}.rail-acts{grid-template-columns:1fr 1fr;gap:6px;display:grid}.rail-act{border:1px solid var(--border);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:8px;flex-direction:column;justify-content:center;align-items:center;gap:5px;padding:9px 4px 7px;font-size:12px;display:flex}.rail-act:hover{background:var(--sel)}.archive-target{color:var(--fg);word-break:break-all;margin:2px 0 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.archive-open{color:var(--muted);margin:4px 0 10px;padding-left:18px;font-size:12.5px}.dialog-shade{z-index:10;background:#00000080;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.dialog{border:1px solid var(--border);background:var(--panel);border-radius:10px;flex-direction:column;max-width:340px;max-height:100%;padding:0;display:flex;overflow:hidden}.dialog>*{flex-shrink:0}.dialog>.rail-head{border-bottom:1px solid var(--border);margin:0;padding:14px 18px}.dialog-body{flex-shrink:1;min-height:0;padding:14px 18px;overflow-y:auto}.dialog p{margin:0 0 12px}.dialog>.dialog-body>:last-child{margin-bottom:0}.dialog>.notice{margin:0;padding:12px 18px}.dialog-acts{border-top:1px solid var(--border);justify-content:flex-end;gap:10px;padding:14px 18px;display:flex}.dialog-acts button{border:1px solid var(--border);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:4px 12px;font-size:12px}.dialog-acts .send{border-color:var(--accent);width:auto}.board-list{margin:0;padding:0;list-style:none}.board-stage{padding:2px 0;font-size:12px}.board-stage-name{color:var(--muted);margin:0}.board-stage.is-running .board-stage-name,.board-stage.is-done .board-stage-name{color:var(--fg)}.board-mark{width:14px;color:var(--muted);display:inline-block}.board-stage.is-running .board-mark{color:var(--accent)}.board-stage.is-done .board-mark{color:var(--ok)}.board-stage-name button{color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;padding:0}.board-stage-name button:hover{color:var(--accent);text-decoration:underline}.board-chevron{color:var(--muted);margin-left:4px;font-size:9px}.board-artifacts{margin:2px 0 4px 20px;padding:0;list-style:none}.board-artifacts button{color:var(--accent);font:inherit;cursor:pointer;text-align:left;background:0 0;border:none;padding:1px 0;font-size:11.5px}.board-now{color:var(--accent);align-items:center;gap:6px;margin:0;font-size:11.5px;display:flex}.board-now .pulse{background:var(--accent);border-radius:50%;flex:none;width:6px;height:6px;animation:1.6s ease-in-out infinite board-pulse}@keyframes board-pulse{50%{opacity:.25}}@media (prefers-reduced-motion:reduce){.board-now .pulse{animation:none}}.form{padding-bottom:40px}.form .lede{color:var(--muted)}.choices{gap:12px;margin:0;padding:0;list-style:none;display:grid}.choices li{border:1px solid var(--border);border-radius:8px;padding:10px 12px}.choices li.is-recommended{border-color:var(--accent)}.choices label{cursor:pointer;align-items:center;gap:8px;display:flex}.choice-title{font-weight:600}.choice-flag{color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-left:8px;font-size:10.5px;font-weight:400}.choice-about{color:var(--muted);margin:4px 0 0 22px;font-size:12.5px}.choice-why{margin:4px 0 0 22px;font-size:12.5px}.choice-mode{cursor:pointer;align-items:flex-start;gap:8px;display:flex}.field{margin-bottom:12px;display:block}.field>span{color:var(--muted);margin-bottom:4px;font-size:11.5px;display:block}.field input,.field-dump,.run-with select,.run-with input,.extra select,.extra input{background:var(--bg);color:var(--fg);border:1px solid var(--border);font:inherit;border-radius:6px;padding:5px 8px;font-size:12.5px}.field input,.field-dump{box-sizing:border-box;resize:vertical;border-radius:7px;width:100%;padding:7px 9px;font-size:13px}.field-dump{min-height:140px}.attached{gap:4px;margin:0 0 12px;padding:0;font-size:12px;list-style:none;display:grid}.attached li{justify-content:space-between;gap:8px;display:flex}.attached button{color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:none;font-size:11px}.form-acts{justify-content:flex-end;gap:10px;margin-top:20px;display:flex}.form-acts button{border:1px solid var(--border);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:5px 14px;font-size:12.5px}.form-acts .send{border-color:var(--accent);width:auto;color:var(--accent)}.dialog-wide{width:90%;max-width:560px}.doc-and-ask{flex-direction:column;min-width:0;display:flex;overflow-y:auto}.doc-and-ask .doc{flex:1 0 auto;overflow-y:visible}.ask{border:1px solid var(--accent);background:var(--panel);border-radius:10px;width:calc(100% - 80px);max-width:780px;margin:0 auto 40px;padding:16px 20px}.doc>.ask{width:100%;max-width:none;margin:14px 0 28px;scroll-margin-top:12px}.ask-head h2{margin:0;font-size:15px}.ask-about{color:var(--muted);margin:2px 0 10px;font-size:11.5px}.ask-about b{color:var(--fg);font-weight:500}.ask .prose{font-size:inherit}.ask .prose>:last-child{margin-bottom:0}.ask-asks{margin:0 0 12px}.ask-asked{color:var(--muted);margin:12px 0 0;font-size:12px}.form-acts .ask-question{margin-right:auto}.form-acts button:disabled{color:var(--muted);border-color:var(--border);cursor:default}.questions{gap:18px;margin:0 0 14px;padding-left:18px;display:grid}.question-ask{margin:0 0 8px}.question-ask>p:first-child{font-weight:600}.question-said{box-sizing:border-box;background:var(--bg);border:1px solid var(--border);width:100%;min-height:48px;color:inherit;font:inherit;resize:vertical;border-radius:7px;margin-top:8px;padding:6px 8px;font-size:12.5px}.board-review{padding:3px 0;font-size:12px}.board-marks{vertical-align:-1px;gap:6px;margin-left:8px;display:inline-flex}.board-flag{color:var(--muted);cursor:default;display:inline-flex;position:relative}.board-tip{white-space:nowrap;max-width:none;padding:4px 8px}.board-flag.is-waiting{opacity:.45}.board-flag.is-ran{color:var(--fg);opacity:1}.board-flag:hover{color:var(--fg)}.board-log{border-top:1px solid var(--border);margin-top:16px;padding-top:6px}.board-log>.rail-head:first-child{margin-top:6px}.board-under-name{color:var(--muted);margin:4px 0 2px;font-size:12px}.board-nested>.board-under{list-style:none}.board-nested .board-list{border-left:1px solid var(--border);margin-left:10px;padding-left:8px}.run-field{grid-template-columns:92px 1fr;align-items:center;gap:10px;margin:0 0 10px;display:grid}.run-field>span,.run-head{letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-size:11px}.run-with{align-items:center;gap:8px;display:flex}.run-field .run-with select{min-width:150px;font-size:13px}.run-with input{flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.run-phases{flex-direction:column;gap:2px;margin:-4px 0 10px 102px;display:flex}.run-phases label{align-items:center;gap:7px;font-size:12.5px;display:flex}.run-head{margin:16px 0 6px;font-weight:400}.run-banner{border:1px solid var(--border);background:var(--sunken);border-radius:8px;margin:0 0 14px;padding:8px 12px;font-size:12.5px}.run-checkpoint{border:1px solid var(--border);border-radius:8px;margin:6px 0;padding:9px 12px}.run-checkpoint-head{align-items:baseline;gap:8px;font-size:13px;display:flex}.run-why{color:var(--muted);margin:0;font-size:12px}.run-phases label+.run-why,.run-phases .run-stale+.run-why{margin-top:4px}.run-why b{color:var(--fg);font-weight:400}.run-checkpoint .run-why{margin:2px 0 0 24px}.run-panel{flex-wrap:wrap;gap:5px;margin:7px 0 0 24px;display:flex}.run-reviewer{background:var(--sunken);border:1px solid var(--border);border-radius:12px;align-items:flex-start;gap:6px;max-width:100%;padding:2px 9px;font-size:12px;display:inline-flex}.run-focus{color:inherit;font:inherit;text-align:left;cursor:pointer;overflow-wrap:anywhere;background:0 0;border:none;padding:0}.run-focus:hover{color:var(--accent)}.run-new{border:1px dashed var(--border);border-radius:12px;flex:220px;align-items:center;gap:6px;max-width:100%;padding:2px 9px;font-size:12px;display:inline-flex}.run-new input{min-width:10ch;color:inherit;font:inherit;background:0 0;border:none;flex:1;padding:0}.run-new input:focus{color:var(--accent);outline:none}.run-drop,.run-new button{color:var(--muted);cursor:pointer;font:inherit;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-size:12px;display:inline-flex}.run-drop{padding-top:3px}.run-drop:hover,.run-new button:hover{color:var(--fg)}.run-stale{color:var(--warn);margin:6px 0 0;font-size:12px}.field-slug{color:var(--muted);font-size:12px}.field-slug code{background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:0 4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px}.flavour{align-items:baseline;gap:8px;margin:4px 0;display:flex}.flavour-what{font-size:13px}.flavour-hint{color:var(--muted);margin-top:1px;font-size:12px;display:block}.extras{border-left:2px solid var(--border);flex-direction:column;gap:6px;margin:6px 0 2px 24px;padding-left:10px;display:flex}.extra{grid-template-columns:78px 1fr;align-items:center;gap:8px;display:grid}.extra>span{color:var(--muted);font-size:12px}.extra select{max-width:260px}.extra select,.extra input{padding:4px 8px}.choice-locked{color:var(--muted)}.board-head{justify-content:space-between;align-items:center;gap:8px;display:flex}.board-amend{color:var(--muted);cursor:pointer;background:0 0;border:none;align-items:center;padding:0;display:inline-flex}.board-amend:hover{color:var(--accent)}.ask-offer{align-items:baseline;gap:8px;margin:10px 0 0;font-size:13px;display:flex}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.hm-page{height:100%;padding:14px 16px 40px;font-size:13.5px;position:relative;overflow:hidden auto}.hm-column{min-width:0;max-width:1100px;margin:0 auto}.hm-head{color:var(--muted);flex-wrap:wrap;align-items:center;gap:6px 16px;margin-bottom:14px;font-size:12.5px;display:flex}.hm-repo{color:var(--fg);font-size:16px;font-weight:600}.hm-dot{background:var(--ok);vertical-align:middle;border-radius:50%;width:8px;height:8px;margin-right:5px;display:inline-block}.hm-dot.hm-dirty{background:var(--warn)}.hm-read{align-items:center;gap:8px;margin-left:auto;display:flex}.hm-read button{font:inherit;color:var(--fg);background:var(--panel);border:1px solid var(--border);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:4px 10px;font-size:12.5px;display:flex}.hm-read button:hover{border-color:var(--muted)}.hm-read button[disabled]{cursor:default;color:var(--muted)}.hm-plain{color:var(--muted);margin:0;padding:20px 0}.hm-failed{color:var(--fg);border-left:3px solid var(--warn);flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 14px;padding:8px 12px;font-size:12.5px;display:flex}.hm-bar{background:var(--panel);border-radius:5px;width:90px;height:10px;display:inline-block}.hm-bar-name{width:120px;height:14px}@media (prefers-reduced-motion:no-preference){.hm-bar{animation:1.6s ease-in-out infinite hmBreathe}@keyframes hmBreathe{0%,to{opacity:1}50%{opacity:.55}}}.hm-card{background:var(--panel);border:1px solid var(--border);border-radius:8px;min-width:0;margin-bottom:12px;padding:12px 14px}.hm-card h3{text-transform:uppercase;letter-spacing:.06em;color:var(--muted);align-items:baseline;gap:8px;margin:0 0 8px;font-size:11.5px;font-weight:600;display:flex}.hm-card h3 small{text-transform:none;letter-spacing:0;font-weight:400}.hm-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));gap:12px;display:grid}.hm-grid .hm-card{margin-bottom:12px}.hm-tiles{grid-template-columns:repeat(auto-fit,minmax(min(100%,180px),1fr));gap:12px;display:grid}.hm-big{font-variant-numeric:tabular-nums;font-size:24px;font-weight:600;line-height:1.15}.hm-lines{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:20px}.hm-lines .hm-added{color:#7bb992;white-space:nowrap}.hm-lines .hm-deleted{color:#c78b8b;white-space:nowrap}.hm-lines .hm-slash{color:var(--muted)}.hm-span{color:var(--muted);font-size:14px;font-weight:400}.hm-sub{color:var(--muted);font-size:12px}.hm-up{color:var(--ok)}.hm-muted{color:var(--muted)}.hm-cut{color:var(--muted);margin:10px 0 0;font-size:12px}.hm-loader{background:linear-gradient(90deg,#1b2230 25%,#232b38 37%,#1b2230 63%) 0 0/400% 100%;border-radius:6px}@media (prefers-reduced-motion:no-preference){.hm-loader{animation:1.4s infinite hmShimmer}@keyframes hmShimmer{0%{background-position:100% 0}to{background-position:0 0}}}.hm-notice{color:var(--fg);margin:0;font-size:12.5px}.hm-tip{opacity:0;white-space:nowrap;background:#1b2230;max-width:none;padding:4px 8px;transition:opacity .12s}.hm-tip.hm-tip-on{opacity:1}.hm-scroll{scrollbar-width:thin;padding-bottom:4px;overflow:auto hidden}.hm-garden-wrap{position:relative}.hm-garden-wrap.hm-more:before{content:"";background:linear-gradient(90deg, var(--panel), transparent);z-index:2;pointer-events:none;width:64px;position:absolute;top:0;bottom:22px;left:0}.hm-earlier{z-index:3;color:var(--muted);background:var(--panel);border-radius:6px;align-items:center;gap:6px;padding:2px 6px;font-size:11.5px;display:flex;position:absolute;top:8px;left:8px}@media (prefers-reduced-motion:no-preference){.hm-earlier i{animation:1.6s ease-in-out infinite hmNudge;display:inline-block}@keyframes hmNudge{50%{transform:translate(-4px)}}}.hm-garden{background:linear-gradient(#0000 78%,#12281a 100%);border-radius:6px 6px 0 0;min-width:max-content;display:block}.hm-stem{stroke:#2f7a3e;stroke-width:2px;fill:none}.hm-leaf{fill:#26a641}.hm-bud{fill:#39d353}.hm-flower{fill:#a371f7}.hm-flower.hm-shade-1{fill:#b168e2}.hm-flower.hm-shade-2{fill:#c060c9}.hm-flower.hm-shade-3{fill:#c95aa5}.hm-flower.hm-shade-4{fill:#d55181}@media (prefers-reduced-motion:no-preference){.hm-plant.hm-sway{animation:3.2s ease-in-out infinite alternate hmSway}@keyframes hmSway{0%{transform:rotate(-2deg)}to{transform:rotate(2deg)}}}.hm-axis{border-top:1px solid var(--border);min-width:max-content;height:22px;color:var(--muted);font-size:11px;position:relative}.hm-axis span{white-space:nowrap;position:absolute;top:3px}.hm-axis span:before{content:"";border-left:1px solid var(--border);height:5px;position:absolute;top:-4px;left:0}.hm-axis span.hm-year{color:var(--fg);font-weight:600;top:11px}.hm-legend{color:var(--muted);flex-wrap:wrap;align-items:center;gap:16px;margin-top:8px;font-size:12px;display:flex}.hm-legend b{color:var(--fg)}.hm-info{all:unset;box-sizing:border-box;width:14px;height:14px;color:var(--muted);vertical-align:-2px;cursor:default;justify-content:center;align-items:center;margin-left:5px;display:inline-flex}.hm-info:hover,.hm-info:focus-visible{color:var(--fg)}.hm-infotip{width:280px}.hm-key{vertical-align:middle;border-radius:50%;width:8px;height:8px;margin-right:5px;display:inline-block}.hm-bud-key{background:#39d353}.hm-flower-key{background:#a371f7}.hm-big-key{background:#d55181}.hm-bars{align-items:flex-end;gap:3px;height:64px;display:flex}.hm-bars.hm-bars-wide{gap:8px}.hm-bars i{background:#006d32;border-radius:3px 3px 0 0;flex:1;min-width:3px}.hm-bars i.hm-tall{background:#39d353}.hm-bars i:hover{background:var(--accent)}.hm-axis-x{color:var(--muted);justify-content:space-between;margin-top:4px;font-size:11px;display:flex}.hm-hbar{grid-template-columns:78px 1fr 44px;align-items:center;gap:8px;margin:5px 0;font-size:12px;display:grid}.hm-key-name{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.hm-track{background:var(--sunken);border-radius:4px;height:8px;overflow:hidden}.hm-track i{border-radius:0 4px 4px 0;height:100%;display:block}.hm-value{text-align:right;color:var(--muted);font-variant-numeric:tabular-nums}.hm-areas .hm-hbar{grid-template-columns:minmax(64px,120px) 1fr auto 44px}.hm-shift{text-align:right;color:var(--muted);font-variant-numeric:tabular-nums}.hm-list{max-height:260px;font-size:12.5px;overflow-y:auto}.hm-list>div{border-bottom:1px dashed #21262d;grid-template-columns:1fr auto auto auto;align-items:center;gap:10px;padding:5px 0;display:grid}.hm-list>div:last-child{border-bottom:0}.hm-name{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.hm-flag{color:var(--muted);white-space:nowrap;background:#1f2937;border-radius:9px;padding:1px 7px;font-size:11px}.hm-flag.hm-merged,.hm-flag.hm-squash{color:var(--ok);background:#3fb95026}.hm-flag.hm-stale{color:var(--warn);background:#d2992226}.hm-flag.hm-current{color:var(--accent);background:#58a6ff26}.hm-pop{position:relative}.hm-del{color:var(--muted);cursor:pointer;background:0 0;border:0;padding:2px 4px;line-height:0}.hm-del:hover{color:var(--fg)}.hm-popover{z-index:5;background:var(--panel);border:1px solid var(--border);white-space:normal;border-radius:8px;width:min(420px,88vw);padding:10px 12px;font-size:12.5px;position:absolute;top:26px;right:0;box-shadow:0 8px 28px #00000080}.hm-popover p{color:var(--muted);margin:0 0 6px}.hm-warn-text{color:var(--warn)}.hm-cmd{align-items:center;gap:8px;display:flex}.hm-cmd input{background:var(--sunken);color:var(--fg);border:1px solid var(--border);border-radius:6px;flex:1;min-width:0;padding:6px 8px;font:12px ui-monospace,SFMono-Regular,Menlo,monospace}.hm-hint{color:var(--muted);white-space:nowrap;font-size:11px}.hm-bee{z-index:8;appearance:none;aspect-ratio:110/88;cursor:pointer;will-change:transform;visibility:hidden;background:0 0;border:0;width:110px;padding:0;position:fixed;top:0;left:0}.hm-bee.hm-here{visibility:visible}.hm-bee .hm-face{transform-origin:50%;width:100%;height:100%;display:block}.hm-bee img{object-fit:contain;pointer-events:none;width:100%;height:100%;display:block}.hm-bee.hm-hop .hm-face{animation:.5s cubic-bezier(.3,0,.2,1) hmHop}@keyframes hmHop{40%{transform:translateY(-14px)}}.hm-page.hm-still .hm-head{padding-right:76px}.hm-bee.hm-parked{width:64px;height:52px;position:absolute;top:4px;left:auto;right:12px;transform:none}@media (prefers-reduced-motion:reduce){.hm-bee.hm-hop .hm-face{animation:none}}.hm-bubble{z-index:9;background:var(--fg);color:#0d1117;opacity:0;pointer-events:none;border-radius:10px;max-width:280px;padding:10px 12px;font-size:12.5px;line-height:1.4;transition:opacity .2s,transform .2s;position:fixed;transform:translateY(6px);box-shadow:0 8px 28px #00000080}.hm-who{color:#4b5563;margin-top:4px;display:block}.hm-bubble.hm-said{opacity:1;transform:translateY(0)}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{A as e,C as t,D as n,E as r,O as i,c as a,i as o,k as s,n as c,r as l,s as u,t as d,y as f}from"./api-BBDf_rzz.js";import{t as p}from"./useNow-BHFKFraS.js";var m=e(),h=i(),g=n();function _(e){return(0,g.jsxs)(`div`,{className:`hm-hbar`,children:[(0,g.jsx)(`span`,{className:`hm-key-name`,title:e.name,children:e.name}),(0,g.jsx)(`div`,{className:`hm-track`,children:(0,g.jsx)(`i`,{style:{width:`${e.width}%`,background:e.colour}})}),(0,g.jsx)(`span`,{className:`hm-value`,children:e.value}),e.note!==void 0&&(0,g.jsx)(`span`,{className:e.note.className,title:e.note.title,children:e.note.said})]})}var v=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`];function y(e){return e.toLocaleString(`en-GB`)}function b(e,t){return`${y(e)} ${t}${e===1?``:`s`}`}function x(e){let t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(e);if(t===null)return null;let n=Number(t[2])-1;return n<0||n>11?null:{year:t[1]??``,month:n,date:Number(t[3])}}function S(e){if(e===null)return``;let t=x(e.slice(0,10));return t===null?``:`${t.date} ${v[t.month]} ${t.year}`}function C(e){let t=x(e.slice(0,10));return t===null?``:`${t.date} ${v[t.month]}`}function ee(e,t){let n=x(e.slice(0,10)),r=x(t.slice(0,10));return n===null||r===null?``:n.year===r.year&&n.month===r.month?`${n.date}-${r.date} ${v[n.month]}`:`${C(e)} - ${C(t)}`}function te(e){return e===0?`working tree clean`:`${b(e,`uncommitted change`)}`}function ne(e,t){return`+${y(e)} / -${y(t)}`}var re=`#199e70`;function ie(e,t){if(t===0)return{said:e===0?`quiet`:`new`,up:e>0};let n=Math.round((e-t)/t*100);return{said:`${n>=0?`+`:``}${n}%`,up:n>0}}function ae(e){if(e.areas.length===0)return(0,g.jsx)(`p`,{className:`hm-plain hm-muted`,children:`No commit in the last sixty days.`});let t=Math.max(1,...e.areas.map(e=>e.lines));return(0,g.jsx)(`div`,{className:`hm-areas`,children:e.areas.map(e=>{let n=ie(e.lines,e.before.lines);return(0,g.jsx)(_,{name:e.area,width:e.lines/t*100,colour:re,value:`${y(e.lines)} lines · ${b(e.commits,`commit`)}`,note:{said:n.said,className:n.up?`hm-shift hm-up`:`hm-shift`,title:`${y(e.before.lines)} lines in ${b(e.before.commits,`commit`)} over the thirty days before`}},e.area)})})}var oe=/^[A-Za-z0-9._/-]+$/;function se(e){return oe.test(e)?e:`'${e.replace(/'/g,`'\\''`)}'`}function ce(e){return e===`merged`||e===`squash`||e===`stale`}function le(e,t){return`git branch ${t===`merged`?`-d`:`-D`} -- ${se(e)}`}function ue(e,t){switch(e){case`current`:return`current`;case`default`:return`default`;case`merged`:return`merged`;case`squash`:return`merged (squash)`;case`stale`:return`stale`;default:return`ahead of ${t}`}}function de(e){let t=e.filter(e=>e.state===`merged`||e.state===`squash`).length,n=e.filter(e=>e.state===`stale`).length;return`${y(e.length)} local · ${y(t)} merged, ${y(n)} stale`}function fe(e,t){return e===`stale`?{said:`This branch holds work that is not on ${t}. The command deletes it anyway.`,warn:!0}:e===`squash`?{said:`Squash-merged into ${t}. git cannot see a squash merge, so the force flag is needed. Home checked that the whole change is on ${t}.`,warn:!1}:{said:`Merged into ${t}, so the safe delete works.`,warn:!1}}function pe(e){let t=e.default??`the default branch`,[n,i]=(0,m.useState)(null),a=(0,m.useRef)(null);return(0,m.useEffect)(()=>{if(n===null)return;let e=e=>{a.current?.contains(e.target)!==!0&&i(null)};return document.addEventListener(`click`,e),()=>document.removeEventListener(`click`,e)},[n]),(0,g.jsx)(`div`,{className:`hm-list`,children:e.branches.map(o=>{let s=o.ahead>0?` · ${b(o.ahead,`commit`)} ahead of ${t}`:``;return(0,g.jsxs)(`div`,{children:[(0,g.jsx)(`span`,{className:`hm-name`,title:`${o.name}${s}`,children:o.name}),(0,g.jsx)(`span`,{className:`hm-muted`,children:u(o.lastCommit,e.now)}),(0,g.jsx)(`span`,{className:`hm-flag hm-${o.state}`,children:ue(o.state,t)}),(0,g.jsx)(`span`,{className:`hm-pop`,children:ce(o.state)&&(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`button`,{type:`button`,className:`hm-del`,title:`Delete this branch`,onClick:e=>{e.stopPropagation(),i(n===o.name?null:o.name)},children:(0,g.jsx)(r,{size:14})}),n===o.name&&(0,g.jsx)(me,{branch:o,into:t,held:a})]})})]},o.name)})})}function me(e){let t=fe(e.branch.state,e.into),n=le(e.branch.name,e.branch.state);return(0,g.jsxs)(`div`,{className:`hm-popover`,ref:e.held,children:[(0,g.jsxs)(`p`,{children:[(0,g.jsx)(`span`,{className:t.warn?`hm-warn-text`:void 0,children:t.said}),` Deletes the local branch alone: a remote branch of the same name is not touched.`]}),(0,g.jsxs)(`div`,{className:`hm-cmd`,children:[(0,g.jsx)(`input`,{readOnly:!0,value:n,"aria-label":`The command that deletes this branch`,onClick:e=>e.currentTarget.select()}),(0,g.jsx)(`span`,{className:`hm-hint`,children:`click selects · copy with the keyboard`})]})]})}function w(e){let{title:t,note:n,state:r,said:i,loaderHeight:a=120}=e;return(0,g.jsxs)(`section`,{className:e.className===void 0?`hm-card`:`hm-card ${e.className}`,children:[t!==void 0&&(0,g.jsxs)(`h3`,{children:[t,r===`read`&&n!==void 0&&(0,g.jsx)(`small`,{children:n})]}),r===`loading`?(0,g.jsx)(`div`,{className:`hm-loader`,style:{height:`${a}px`}}):r===`failed`?(0,g.jsxs)(`p`,{className:`hm-notice`,children:[(0,g.jsx)(`span`,{children:i}),` `,(0,g.jsx)(`span`,{className:`hm-muted`,children:`Press Refresh to try again.`})]}):e.children]})}var he=``+new URL(`bee-B7t97l5B.webp`,import.meta.url).href,ge=``+new URL(`buzz-CUUsbnXH.mp3`,import.meta.url).href,_e=``+new URL(`pop-D5ZANBC3.mp3`,import.meta.url).href,T=[{said:`Simplicity is prerequisite for reliability.`,who:`Edsger W. Dijkstra`},{said:`A system's design mirrors the communication structure of the organization that built it.`,who:`Melvin Conway (Conway's law)`}],ve=640,E=[50,70],ye=6e3,D=[.15,.75],be=[95,125],xe=16,O=[14,32],k=[2.2,3.4],Se=.6,A=[ge,_e];function j(e,t){return e+Math.random()*(t-e)}function M(){return window.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches===!0}function Ce(e){let t=(0,m.useRef)(null),n=(0,m.useRef)(null),[r,i]=(0,m.useState)(null),a=(0,m.useRef)(e.sound);a.current=e.sound;let o=(0,m.useRef)(0),s=(0,m.useRef)(!1),c=(0,m.useRef)({x:0,y:0,base:0,amplitude:0,period:3,t:0,direction:1,speed:110,tilt:0,away:j(E[0],E[1]),flying:!1}),l=(0,m.useCallback)(()=>{if(s.current)return;if(s.current=!0,i(o.current%T.length),o.current+=1,a.current)try{let e=new Audio(A[(o.current-1)%A.length]);e.volume=.7,e.play().catch(e=>{console.warn(`[home] the bee could not play its clip:`,e)})}catch(e){console.warn(`[home] the bee could not play its clip:`,e)}let e=t.current;e!==null&&(e.classList.remove(`hm-hop`),e.offsetWidth,e.classList.add(`hm-hop`)),setTimeout(()=>{s.current=!1,i(null)},ye)},[]);(0,m.useEffect)(()=>{let e=t.current;if(e===null)return;let r=()=>Math.round(110*Math.min(1,window.innerWidth/ve));if(M()){e.style.width=`${r()}px`,e.classList.add(`hm-parked`,`hm-here`);return}let i=0,a=0,o=performance.now(),l=()=>{let t=c.current;e.style.transform=`translate(${t.x}px, ${t.y}px) scaleX(${t.direction}) rotate(${t.direction*t.tilt}deg)`},u=t=>{let d=Math.min(.05,(t-o)/1e3);o=t;let f=c.current,p=r();if(p!==i&&(i=p,e.style.width=`${p}px`),f.flying){let t=s.current?xe:f.speed;f.t+=d,f.x+=f.direction*t*d;let n=f.t/f.period*2*Math.PI;f.y=f.base+Math.sin(n)*f.amplitude;let r=Math.cos(n)*f.amplitude*2*Math.PI/f.period;f.tilt=Math.atan2(r,t)*180/Math.PI*Se,(f.x<-p*1.5||f.x>window.innerWidth+p*.5)&&!s.current&&(f.flying=!1,f.away=j(E[0],E[1]),e.classList.remove(`hm-here`))}else{if(f.away-=d,f.away>0){a=requestAnimationFrame(u);return}f.direction=Math.random()<.5?1:-1,f.x=f.direction>0?-p:window.innerWidth,f.amplitude=j(O[0],O[1]);let t=window.innerHeight*D[0]+f.amplitude,n=window.innerHeight*D[1]-f.amplitude;f.base=n>t?j(t,n):(t+n)/2,f.y=f.base,f.tilt=0,f.period=j(k[0],k[1]),f.speed=j(be[0],be[1]),f.t=0,f.flying=!0,e.classList.add(`hm-here`)}l();let m=n.current;m!==null&&(m.style.left=`${Math.min(Math.max(8,c.current.x-110),window.innerWidth-300)}px`,m.style.top=`${Math.max(8,c.current.y-92)}px`),a=requestAnimationFrame(u)};return a=requestAnimationFrame(u),()=>cancelAnimationFrame(a)},[]);let u=r===null?null:T[r];return(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`button`,{type:`button`,className:`hm-bee`,ref:t,"aria-label":`The bee`,title:`A quote`,onClick:l,children:(0,g.jsx)(`span`,{className:`hm-face`,children:(0,g.jsx)(`img`,{src:he,alt:``})})}),(0,g.jsx)(`div`,{className:u==null?`hm-bubble`:`hm-bubble hm-said`,ref:n,children:u!=null&&(0,g.jsxs)(g.Fragment,{children:[u.said,(0,g.jsxs)(`span`,{className:`hm-who`,children:[`- `,u.who]})]})})]})}var N=10,P=170,we=10,Te=128,F=12,I=60,L=500,R=2e3,z=`Its height follows the lines changed that day, added plus deleted, on a square-root scale so a huge day does not flatten the rest.`,B=`plant-height`,Ee=5,De=.7;function V(e){return e.added+e.deleted}function Oe(e){let t=2166136261;for(let n=0;n<e.length;n++)t^=e.charCodeAt(n),t=Math.imul(t,16777619);return(t>>>0)%1e4/1e4}function ke(e,t){let n=Math.min(Math.max((t-L)/(R-L),0),1),r=Math.min(Math.max(n+(Oe(e)-.5)*De,0),1);return Math.round(r*(Ee-1))}function H(e){let[t=0,n=1,r=1]=e.split(`-`).map(Number);return Date.UTC(t,n-1,r)}function U(e,t){return Math.round((H(t)-H(e))/864e5)}function Ae(e,t,n){if(n<=0)return[0,0];let r=t-I,i=t+n+I,a=0;for(;a<e.length&&(e[a]??0)<r;)a+=1;let o=a;for(;o<e.length&&(e[o]??0)<=i;)o+=1;return[a,o]}function je(e,t){let n=[],r=``,i=``;for(let a=0;a<t;a+=1){let t=new Date(H(e)+a*864e5).toISOString().slice(0,10),o=F+a*N;t.slice(0,4)!==i&&(i=t.slice(0,4),n.push({x:o,said:i,year:!0})),t.slice(0,7)!==r&&(r=t.slice(0,7),n.push({x:o,said:C(t).split(` `)[1]??``,year:!1}))}return n}function Me(e){let{at:t,lines:n}=e,r=we+Math.sqrt(n)/e.tallest*Te,i=P-r,a=e.index%2==0?-3:3,o=P-r*.55;return(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`path`,{className:`hm-stem`,d:`M${t},${P} q${a},${-r/2} 0,${-r}`}),(0,g.jsx)(`ellipse`,{className:`hm-leaf`,cx:t-4,cy:o,rx:4,ry:2,transform:`rotate(-25 ${t-4} ${o})`}),n>=L?(0,g.jsx)(`circle`,{className:`hm-flower hm-shade-${ke(e.date,n)}`,cx:t,cy:i,r:n>=R?5.5:3.5}):(0,g.jsx)(`circle`,{className:`hm-bud`,cx:t,cy:i,r:2})]})}function Ne(e){let{days:t,today:n}=e,r=o({className:`hm-infotip`,side:`top`}),i=(0,m.useRef)(null),a=(0,m.useRef)(null),s=(0,m.useRef)([]),c=(0,m.useRef)([0,0]),[l,u]=(0,m.useState)(!1),d=t[0]?.date??n,p=t[t.length-1]?.date??n,h=Math.max(1,U(d,p>n?p:n)+1),_=h*N+F*2,v=Math.sqrt(Math.max(1,...t.map(V))),y=(0,m.useMemo)(()=>t.filter(e=>V(e)>0).map(e=>({day:e,x:F+U(d,e.date)*N})),[t,d]),x=(0,m.useCallback)(()=>{let e=i.current,t=a.current;if(e===null||t===null)return;let n=t.querySelectorAll(`g.hm-plant`),[r,o]=Ae(s.current,e.scrollLeft,e.clientWidth),[l,d]=c.current;for(let e=l;e<d;e+=1)(e<r||e>=o)&&n[e]?.classList.remove(`hm-sway`);for(let e=r;e<o;e+=1)n[e]?.classList.add(`hm-sway`);c.current=[r,o],u(e.scrollLeft>4)},[]);return(0,m.useEffect)(()=>{let e=i.current;if(e===null)return;s.current=y.map(e=>e.x),c.current=[0,0],e.scrollLeft=e.scrollWidth,x();let t=new ResizeObserver(x);return t.observe(e),()=>t.disconnect()},[x,y]),(0,g.jsxs)(g.Fragment,{children:[(0,g.jsxs)(`div`,{className:l?`hm-garden-wrap hm-more`:`hm-garden-wrap`,children:[l&&(0,g.jsxs)(`div`,{className:`hm-earlier`,children:[(0,g.jsx)(`i`,{children:`‹`}),` earlier`]}),(0,g.jsxs)(`div`,{className:`hm-scroll`,ref:i,onScroll:x,"data-testid":`garden-scroll`,children:[(0,g.jsx)(`svg`,{className:`hm-garden`,ref:a,width:_,height:P,role:`img`,"aria-label":`One plant for each day worked on since ${S(d)}`,children:y.map(({day:t,x:n},r)=>(0,g.jsx)(`g`,{className:`hm-plant`,"data-day":t.date,style:{transformOrigin:`${n}px ${P}px`,animationDelay:`${r%7*-.4}s`},...e.hover(`${S(t.date)} · ${ne(t.added,t.deleted)} · ${b(t.commits,`commit`)}`),children:(0,g.jsx)(Me,{at:n,index:r,date:t.date,lines:V(t),tallest:v})},t.date))}),(0,g.jsx)(`div`,{className:`hm-axis`,style:{width:`${_}px`},children:je(d,h).map(e=>(0,g.jsx)(`span`,{className:e.year?`hm-year`:void 0,style:{left:`${e.x}px`},children:e.said},`${e.said}-${e.x}`))})]})]}),(0,g.jsxs)(`div`,{className:`hm-legend`,children:[(0,g.jsxs)(`span`,{children:[(0,g.jsx)(`b`,{children:`Each plant is one day`}),(0,g.jsxs)(`button`,{type:`button`,className:`hm-info`,"aria-label":`What a plant's height means`,"aria-describedby":B,...r.anchor,children:[(0,g.jsx)(f,{size:13}),(0,g.jsx)(`span`,{className:`visually-hidden`,id:B,children:z}),r.says(z)]})]}),(0,g.jsxs)(`span`,{children:[(0,g.jsx)(`i`,{className:`hm-key hm-bud-key`}),`a bud: a day with a change`]}),(0,g.jsxs)(`span`,{children:[(0,g.jsx)(`i`,{className:`hm-key hm-flower-key`}),`a flower: a day of 500 lines or more`]}),(0,g.jsxs)(`span`,{children:[(0,g.jsx)(`i`,{className:`hm-key hm-big-key`}),`a big flower: a day of 2,000 or more`]})]})]})}function Pe(e){let{repository:t,onTop:n,readAt:r,now:i}=e,a=(0,g.jsx)(Fe,{readAt:r,now:i,onRefresh:e.onRefresh});return t===null?(0,g.jsxs)(`div`,{className:`hm-head`,children:[(0,g.jsx)(`span`,{className:`hm-bar hm-bar-name`}),(0,g.jsx)(`span`,{className:`hm-bar`}),(0,g.jsx)(`span`,{className:`hm-bar`}),a]}):(0,g.jsxs)(`div`,{className:`hm-head`,children:[(0,g.jsx)(`span`,{className:`hm-repo`,children:t.name}),(0,g.jsxs)(`span`,{children:[(0,g.jsx)(`span`,{className:t.changedFiles>0?`hm-dot hm-dirty`:`hm-dot`}),t.detached?`${t.branch}, detached`:t.branch,` ·`,` `,te(t.changedFiles)]}),t.commits>0&&(0,g.jsxs)(g.Fragment,{children:[n===null?(0,g.jsx)(`span`,{children:b(t.commits,`commit`)}):(0,g.jsxs)(`span`,{children:[b(n.count,`commit`),` on top of `,n.base,` · `,y(t.commits),` `,`in total`]}),t.firstCommit!==null&&(0,g.jsxs)(`span`,{children:[`since `,S(t.firstCommit),t.shallow&&(0,g.jsx)(`span`,{className:`hm-muted`,children:` (shallow)`})]}),t.lastCommit!==null&&(0,g.jsxs)(`span`,{children:[`last commit `,u(t.lastCommit,i)]})]}),a]})}function Fe(e){return(0,g.jsxs)(`span`,{className:`hm-read`,children:[e.readAt!==null&&(0,g.jsxs)(`span`,{children:[`read `,u(e.readAt.toISOString(),e.now)]}),(0,g.jsxs)(`button`,{type:`button`,onClick:e.onRefresh,title:`Read the repository again`,children:[(0,g.jsx)(t,{size:13}),`Refresh`]})]})}var Ie=s();function Le(){let e=(0,m.useRef)(null),t=(0,m.useRef)(``);return{hover:(0,m.useCallback)(n=>({onMouseMove(r){t.current=n;let i=e.current;if(i===null)return;i.textContent=n;let a=l({left:r.clientX,top:r.clientY,width:0,height:0},{width:i.offsetWidth,height:i.offsetHeight},{width:window.innerWidth,height:window.innerHeight},`top`);i.style.left=`${a.left}px`,i.style.top=`${a.top}px`,i.classList.add(`hm-tip-on`)},onMouseLeave(){t.current===n&&(t.current=``,e.current?.classList.remove(`hm-tip-on`))}}),[]),node:(0,Ie.createPortal)((0,g.jsx)(`div`,{className:`tip hm-tip`,"aria-hidden":`true`,ref:e}),document.body)}}var W=`none`,G=`-`;function Re(e){let{facts:t,now:n}=e,r=t.longestStreak;return(0,g.jsx)(`div`,{className:`hm-list`,children:[[`First commit`,t.firstCommit===null?W:e.shallow?`${S(t.firstCommit)} (shallow)`:S(t.firstCommit),t.firstCommit===null?G:u(t.firstCommit,n)],[`Commits`,y(e.commits),`${t.commitsPerDay} a day`],[`Longest streak`,r===null?W:b(r.days,`day`),r===null?G:ee(r.from,r.to)],[`Biggest day`,t.biggestDay===null?W:S(t.biggestDay.date),t.biggestDay===null?G:b(t.biggestDay.commits,`commit`)],[`Last tag`,t.lastTag===null?W:t.lastTag.name,t.lastTag===null?G:u(t.lastTag.date,n)],[`Stashes`,y(t.stashes),G]].map(([e,t,n])=>(0,g.jsxs)(`div`,{children:[(0,g.jsx)(`span`,{children:e}),(0,g.jsx)(`span`,{className:`hm-muted`,children:t}),(0,g.jsx)(`span`,{className:`hm-muted`,children:n})]},e))})}var K=[`Mon`,`Tue`,`Wed`,`Thu`,`Fri`,`Sat`,`Sun`],ze=[`0`,`6`,`12`,`18`,`23`];function q(e){let t=Math.max(...e.counts);return(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`div`,{className:e.wide===!0?`hm-bars hm-bars-wide`:`hm-bars`,children:e.counts.map((n,r)=>(0,g.jsx)(`i`,{className:n===t&&t>0?`hm-tall`:void 0,style:{height:t>0?`${n/t*100}%`:`0%`},...e.hover(e.said(r,n))},r))}),(0,g.jsx)(`div`,{className:`hm-axis-x`,children:e.marks.map(e=>(0,g.jsx)(`span`,{children:e},e))})]})}function Be(e){return(0,g.jsx)(q,{counts:e.counts,marks:ze,hover:e.hover,said:(e,t)=>`${String(e).padStart(2,`0`)}:00 - ${b(t,`commit`)}`})}function Ve(e){return(0,g.jsx)(q,{counts:e.counts,marks:K,wide:!0,hover:e.hover,said:(e,t)=>`${K[e]} - ${b(t,`commit`)}`})}var J=c(`home`);J.receive;var He=J.credentialsReady,Ue=J.onSession;J.openOutside;var We=J.sayWaiting,Ge=J.listen,Ke=1e4,Y=[`#3987e5`,`#8b949e`,`#c98500`,`#199e70`,`#d95926`,`#d55181`,`#4f74d6`];function qe(e,t){if(e===null)return{size:`ignored files not counted yet`,when:``};let n=a(e.bytes),r=e.largest.map(e=>e.path).join(`, `),i=r===``?``:` (${r})`,o=e.atLeast?`the count hit its ${Ke/1e3} s limit · `:``;return{size:`${e.atLeast?`at least ${n}`:n} ignored${i}`,when:`${o}counted ${u(e.countedAt,t)}`}}function Je(e){let t=qe(e.ignored,e.now);return(0,g.jsxs)(`div`,{children:[e.kinds.map((e,t)=>(0,g.jsx)(_,{name:e.kind,width:e.share,colour:Y[t%Y.length]??``,value:`${e.share}%`},e.kind)),(0,g.jsxs)(`div`,{className:`hm-sub`,children:[b(e.tracked.files,`tracked file`),` · `,y(e.tracked.lines),` lines ·`,` `,a(e.tracked.bytes),` tracked · `,(0,g.jsx)(`b`,{children:t.size}),t.when!==``&&` · ${t.when}`]})]})}var X=d({prefix:`home`,label:`Home`,credentialsReady:He});function Ye(){return X.ask(`history`)}function Xe(){return X.ask(`branches`)}function Ze(){return X.ask(`tree`)}var Z={status:`waiting`};function Qe(e){let t=e instanceof Error?e.message:String(e);return t===``?`the Home tool could not read the repository`:t}function $e(){let[e,t]=(0,m.useState)(Z),[n,r]=(0,m.useState)(Z),[i,a]=(0,m.useState)(Z),[o,s]=(0,m.useState)(null),c=(0,m.useRef)(0),l=(0,m.useRef)(!0);return(0,m.useEffect)(()=>(l.current=!0,()=>{l.current=!1}),[]),{history:e,branches:n,tree:i,readAt:o,read:(0,m.useCallback)(()=>{c.current+=1;let e=c.current,n=()=>l.current&&c.current===e;s(new Date);let i=(e,t)=>{t(Z),e().then(e=>{n()&&t({status:`read`,answer:e})},e=>{n()&&t({status:`failed`,said:Qe(e)})})};i(Ye,t),i(Xe,r),i(Ze,a)},[])}}function et(e){let t=(0,m.useRef)(e);t.current=e,(0,m.useEffect)(()=>{let e=!1,n=new ResizeObserver(n=>{let r=(n[n.length-1]?.contentRect.width??0)>0;r&&!e&&t.current(),e=r});return n.observe(document.documentElement),()=>n.disconnect()},[])}function tt(e){return e===1?`active day`:`active days`}var nt=[`January`,`February`,`March`,`April`,`May`,`June`,`July`,`August`,`September`,`October`,`November`,`December`];function rt(e){return{year:e.getFullYear(),month:e.getMonth(),date:e.getDate()}}function it(e,t,n){let r=new Date(e,t,n),i=String(r.getMonth()+1).padStart(2,`0`),a=String(r.getDate()).padStart(2,`0`);return`${r.getFullYear()}-${i}-${a}`}function Q(e,t,n,r){let i=[];for(let a=0;a<r;a++)i.push(it(e,t,n+a));return i}function at(e){return(e.getDay()+6)%7+1}function ot(e,t){return new Date(e,t+1,0).getDate()}function $(e,t){return t.filter(t=>(e.get(t)?.commits??0)>0).length}function st(e,t){let n=0,r=0;for(let i of t){let t=e.get(i);t!==void 0&&(n+=t.added,r+=t.deleted)}return{added:n,deleted:r}}function ct(e,t,n){if(t===0)return e===0?{said:`none on ${n} either`,up:!1}:{said:`none on ${n}`,up:!0};let r=Math.round((e-t)/t*100);return{said:`${r>=0?`+`:``}${r}% on ${n}`,up:r>0}}function lt(e,t){let n=new Map(e.map(e=>[e.date,e])),r=rt(t),i=Q(r.year,r.month,1,r.date),a=r.month===0?{year:r.year-1,month:11}:{year:r.year,month:r.month-1},o=Q(a.year,a.month,1,Math.min(r.date,ot(a.year,a.month))),s=at(t),c=Q(r.year,r.month,r.date-s+1,s),l=Q(r.year,r.month,r.date-s-6,s),u=st(n,c);return{month:{planted:$(n,i),name:nt[r.month]??``,against:ct($(n,i),$(n,o),`the same days of ${nt[a.month]??``}`)},week:{planted:$(n,c),against:ct($(n,c),$(n,l),`the same days last week`)},lines:{added:u.added,deleted:u.deleted,perDay:Math.round((u.added+u.deleted)/s)}}}function ut(e){let t=lt(e.days,e.now);return(0,g.jsxs)(`div`,{className:`hm-tiles`,children:[(0,g.jsxs)(`div`,{className:`hm-card hm-tile`,children:[(0,g.jsxs)(`div`,{className:`hm-big`,children:[y(t.month.planted),` `,(0,g.jsxs)(`span`,{className:`hm-span`,children:[tt(t.month.planted),` in `,t.month.name]})]}),(0,g.jsx)(`div`,{className:`hm-sub`,children:(0,g.jsx)(dt,{...t.month.against})})]}),(0,g.jsxs)(`div`,{className:`hm-card hm-tile`,children:[(0,g.jsxs)(`div`,{className:`hm-big`,children:[y(t.week.planted),` `,(0,g.jsxs)(`span`,{className:`hm-span`,children:[tt(t.week.planted),` since Monday`]})]}),(0,g.jsx)(`div`,{className:`hm-sub`,children:(0,g.jsx)(dt,{...t.week.against})})]}),(0,g.jsxs)(`div`,{className:`hm-card hm-tile`,children:[(0,g.jsxs)(`div`,{className:`hm-big hm-lines`,children:[(0,g.jsxs)(`span`,{className:`hm-added`,children:[`+`,y(t.lines.added)]}),(0,g.jsx)(`span`,{className:`hm-slash`,children:` / `}),(0,g.jsxs)(`span`,{className:`hm-deleted`,children:[`-`,y(t.lines.deleted)]})]}),(0,g.jsxs)(`div`,{className:`hm-sub`,children:[`lines since Monday · `,y(t.lines.perDay),` a day on average`]})]})]})}function dt(e){return(0,g.jsx)(`span`,{className:e.up?`hm-up`:void 0,children:e.said})}function ft(e){if(e===null||e.default===null)return null;let t=e.branches.find(e=>e.current);return t===void 0||t.name===e.default?null:{count:t.ahead,base:e.default}}function pt(){let e=$e(),n=p(!0),r=Le(),[i,a]=(0,m.useState)(!0);(0,m.useEffect)(()=>Ue(e=>a(e.sound)),[]);let o=M();et(e.read),(0,m.useEffect)(()=>{We(0,!0)},[]);let s=e.history,c=s.status===`read`?s.answer:null,l=c===null?null:c.repository;if(c!==null&&c.repository===null)return(0,g.jsx)(`div`,{className:`hm-page`,children:(0,g.jsx)(`div`,{className:`hm-column`,children:(0,g.jsx)(`p`,{className:`hm-plain`,children:`This workspace holds no git repository, so there is no pulse to show.`})})});let u=l===null?`loading`:`read`,d=e.branches,f=e.tree,h=e=>e.status===`read`?`read`:e.status===`failed`?`failed`:`loading`,_=e=>e.status===`failed`?e.said:void 0,v=d.status===`read`&&d.answer.repository!==null?d.answer:null,y=f.status===`read`&&f.answer.repository!==null?f.answer:null,b=c!==null&&c.repository!==null?c:null,x=l!==null&&l.commits===0,S=(0,g.jsx)(w,{title:`Branches`,note:v===null?void 0:de(v.branches),state:h(d),said:_(d),loaderHeight:130,children:v!==null&&(0,g.jsx)(pe,{branches:v.branches,default:v.default,now:n})}),C=(0,g.jsx)(w,{title:`The tree`,note:`what the workspace is made of`,state:h(f),said:_(f),loaderHeight:130,children:y!==null&&(0,g.jsx)(Je,{kinds:y.kinds,tracked:y.tracked,ignored:y.ignored,now:n})});return s.status===`failed`?(0,g.jsxs)(`div`,{className:o?`hm-page hm-still`:`hm-page`,children:[(0,g.jsxs)(`div`,{className:`hm-column`,children:[(0,g.jsxs)(`div`,{className:`hm-failed`,children:[(0,g.jsx)(`span`,{children:s.said}),` `,(0,g.jsx)(`span`,{className:`hm-muted`,children:`Press Refresh to try again.`}),(0,g.jsx)(`span`,{className:`hm-read`,children:(0,g.jsxs)(`button`,{type:`button`,onClick:e.read,title:`Read the repository again`,children:[(0,g.jsx)(t,{size:13}),`Refresh`]})})]}),(0,g.jsxs)(`div`,{className:`hm-grid`,children:[S,C]})]}),r.node,(0,g.jsx)(Ce,{sound:i})]}):(0,g.jsxs)(`div`,{className:o?`hm-page hm-still`:`hm-page`,children:[(0,g.jsxs)(`div`,{className:`hm-column`,children:[(0,g.jsx)(Pe,{repository:l,onTop:ft(v),readAt:e.readAt,now:n,onRefresh:e.read}),x?(0,g.jsx)(`p`,{className:`hm-plain`,children:`This repository has no commit yet. The page fills in as the work starts.`}):(0,g.jsxs)(g.Fragment,{children:[(0,g.jsxs)(w,{state:u,loaderHeight:192,className:`hm-garden-card`,children:[b!==null&&(0,g.jsx)(Ne,{days:b.days,today:b.readAt.slice(0,10),hover:r.hover}),b?.repository.cut===!0&&(0,g.jsx)(`p`,{className:`hm-cut`,children:`The full history was too large to walk, so this garden is the last two years alone.`})]}),b===null?(0,g.jsxs)(`div`,{className:`hm-tiles`,children:[(0,g.jsx)(`div`,{className:`hm-card hm-tile`,children:(0,g.jsx)(`div`,{className:`hm-loader`,style:{height:`46px`}})}),(0,g.jsx)(`div`,{className:`hm-card hm-tile`,children:(0,g.jsx)(`div`,{className:`hm-loader`,style:{height:`46px`}})}),(0,g.jsx)(`div`,{className:`hm-card hm-tile`,children:(0,g.jsx)(`div`,{className:`hm-loader`,style:{height:`46px`}})})]}):(0,g.jsx)(ut,{days:b.days,now:n}),(0,g.jsxs)(`div`,{className:`hm-grid`,children:[(0,g.jsx)(w,{title:`Rhythm`,note:`commits by hour, author time`,state:u,loaderHeight:84,children:b!==null&&(0,g.jsx)(Be,{counts:b.hours,hover:r.hover})}),(0,g.jsx)(w,{title:`Rhythm`,note:`commits by weekday`,state:u,loaderHeight:84,children:b!==null&&(0,g.jsx)(Ve,{counts:b.weekdays,hover:r.hover})})]}),(0,g.jsxs)(`div`,{className:`hm-grid`,children:[S,(0,g.jsx)(w,{title:`Where the work goes`,note:`lines changed in the last 30 days, against the 30 before`,state:u,loaderHeight:130,children:b!==null&&(0,g.jsx)(ae,{areas:b.areas})})]}),(0,g.jsxs)(`div`,{className:`hm-grid`,children:[C,(0,g.jsx)(w,{title:`Milestones`,state:u,loaderHeight:130,children:b!==null&&(0,g.jsx)(Re,{facts:b.milestones,commits:b.repository.commits,now:n,shallow:b.repository.shallow})})]})]})]}),r.node,(0,g.jsx)(Ce,{sound:i})]})}Ge();var mt=document.getElementById(`root`);mt&&(0,h.createRoot)(mt).render((0,g.jsx)(m.StrictMode,{children:(0,g.jsx)(pt,{})}));
|