office: say exactly what to do next; l opens lazygit and t opens the tool chooser everywhere; a remote run gets a terminal to ask in
This commit is contained in:
@@ -1051,3 +1051,36 @@ Nothing needs rebuilding for any of this — it is all in the commands, which `-
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-now-2026-11-12-07-09-34|Rich discovery: now 2026-11-12 07:09:34]]
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|now]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|now repository structure and configuration patterns]]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Update (2026-11-12 09:39:17)
|
||||
|
||||
paperclip-harness is a Go-based terminal harness with an `office` command (open a folder as a project) and a set of TUI screens invoked via `bin/ha`, including board, work, agents, reply, new, project, repair, lazygit. Binary built from this repo is copied to multiple machines ("office" host aliases `.27` and `.51` referenced) and the active desktop is a 4-screen desktop with aliases `sam-4screen-desktop` and `sam-thinkpad`.
|
||||
|
||||
Key conventions/architecture learned this turn:
|
||||
- The `office` gate on an uninitialized folder is a creation gate: on Y it writes a `.gitignore` containing `.paperclip/`, stages everything, and STOPs without committing (so the user commits in lazygit and re-runs office). `--commit-all` flag creates repo, `.gitignore`, commit, then creates the project with worktrees.
|
||||
- Paperclip worktrees live under `.paperclip/` inside the project folder; therefore `.paperclip/` MUST be in `.gitignore` or a commit will embed worktree checkouts of the repo. `.gitignore` template also suggests `evidence/`, `.env`, `*secret*`.
|
||||
- Base branch is now detected (not hardcoded to main) so repos on `master` work; set in both `policy.baseRef` and workspace `defaultRef`.
|
||||
- Worktree policy is fixed at project creation; `local_path` cannot later become `git_worktree`.
|
||||
- `Project>project` git tab top half = raw git in the folder; bottom half = Paperclip's own close-readiness `close-readiness` scan (states like "1 behind", "merged into base").
|
||||
- A repo-wide warning banner is a wrapper around each screen, not the frame, so it shows once per screen; pressing `R` rebuilds panes and clears it.
|
||||
- `t` in the project screen opens lazygit (0.66.2) in the project folder.
|
||||
|
||||
Workspace paths seen: `/home/sam/paseo/projects/paperclip-harness` (bin/ha), repo `test-alpha` used as a test project with `.paperclip/` ignored. Commit `4be72da` on `main`.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 90%
|
||||
- Reason: Durable architecture, commands, and file conventions for the active pack repo, evidenced across multiple tool outputs.
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-12 09:39:17 -->
|
||||
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]]
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|initializing a folder as a Paperclip project with the `office` gate]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|paperclip-harness: office creation gate + project git tab + per-screen warning banner (commit 4be72da)]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/paperclip-worktree-gitignore-coupling-and-close-readiness-in|Paperclip: worktree/`.gitignore` coupling and close-readiness integration]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-39-17|Rich discovery: with 2026-11-12 09:39:17]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
|
||||
@@ -158,3 +158,159 @@ I have not touched `~/SECURITY`. It is still not a repository, and I will not ru
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-01-33|Rich discovery: with 2026-11-12 09:01:33]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Update (2026-11-12 09:39:17)
|
||||
|
||||
## Rich discovery update (2026-11-12 09:39:17)
|
||||
|
||||
Built, verified on screen, and pushed. Here is what exists now.
|
||||
|
||||
## The gate: `office` never writes history
|
||||
|
||||
Answer **Y** and it stops before committing, deliberately:
|
||||
|
||||
```
|
||||
/tmp/gate1 is not a git repository, so a task cannot have its own branch and worktree.
|
||||
without one, two tasks can change the same files at once, and there
|
||||
is no diff to review and nothing to merge.
|
||||
|
||||
Y make the repository, write .gitignore with .paperclip/, stage everything,
|
||||
and stop. You commit it in lazygit, then run this command again.
|
||||
N create the project in place instead: no branches, no diff, nothing to merge.
|
||||
```
|
||||
|
||||
Verified by running it three ways:
|
||||
|
||||
| input | result |
|
||||
|---|---|
|
||||
| no repo, no terminal, no flag | changes nothing, prints the three commands |
|
||||
| `Y` on a repo with no commit | `wrote .gitignore, ignoring .paperclip/`, staged `.gitignore` and the file, **no commit**, **no project created** |
|
||||
| `--commit-all` | repo, `.gitignore`, commit, then the project is created with worktrees |
|
||||
|
||||
After the staged run, the folder looked like this — which is exactly the state you want to open lazygit in:
|
||||
|
||||
```
|
||||
$ git status --short
|
||||
A .gitignore
|
||||
A a.md
|
||||
$ cat .gitignore
|
||||
# Paperclip puts each task's worktree inside this folder, at .paperclip/.
|
||||
# It is a checkout of this repository, so it must never be committed.
|
||||
.paperclip/
|
||||
|
||||
# Add your own patterns below, one per line. For example:
|
||||
# evidence/
|
||||
# .env
|
||||
# *secret*
|
||||
```
|
||||
|
||||
Two things I fixed while in there, both real faults:
|
||||
|
||||
- the base branch was hardcoded to `main` — a repository on `master` would have been given a policy pointing at a branch that does not exist;
|
||||
- the base branch is now set in both places Paperclip keeps it: the policy's `baseRef` and the workspace's `defaultRef`, which we had been leaving `null`.
|
||||
|
||||
## The `git` tab: live on screen in `Project>project`
|
||||
|
||||
Before fixing `.gitignore` in `test-alpha`:
|
||||
|
||||
```
|
||||
THIS FOLDER
|
||||
repository yes · branch main
|
||||
commits 3
|
||||
remote none — not needed for worktrees
|
||||
changed 0 staged · 0 unstaged · 2 untracked
|
||||
.gitignore missing — .paperclip/ must be in it
|
||||
|
||||
worktrees isolated_workspace · git_worktree · base main
|
||||
|
||||
WHAT TO DO
|
||||
.paperclip/ is not ignored: a commit would swallow every worktree.
|
||||
t lazygit · select .gitignore · e to edit · add .paperclip/
|
||||
Then run R rebuild so every pane is built against the new state.
|
||||
|
||||
WORKTREES, AS PAPERCLIP SEES THEM
|
||||
AUD-172-add-a-status-md-with-one-line clean · 1 behind · merged into base
|
||||
This workspace is 1 commit behind main.
|
||||
```
|
||||
|
||||
and after:
|
||||
|
||||
```
|
||||
.gitignore .paperclip/
|
||||
|
||||
worktrees isolated_workspace · git_worktree · base main
|
||||
|
||||
WORKTREES, AS PAPERCLIP SEES THEM
|
||||
AUD-172-add-a-status-md-with-one-line clean · 1 behind · merged into base
|
||||
```
|
||||
|
||||
The top half is git run in the folder. The bottom half is Paperclip's own
|
||||
`close-readiness` scan, so "1 behind", "merged into base" and the warning text are Paperclip's
|
||||
words, not a second opinion of mine.
|
||||
|
||||
`t` opens lazygit in the project folder. I pressed it, saw lazygit open on the right repository,
|
||||
and closed the pane again — the session is back to eight windows.
|
||||
|
||||
## The warning line: once per screen, not once per panel
|
||||
|
||||
My first attempt put it in the frame, which the board draws three times side by side, so the
|
||||
warning appeared three times on one row. It is now a wrapper around each screen:
|
||||
|
||||
```
|
||||
commit will swallow the worktrees — .paperclip/ is not in .gitignore · Project>project → git
|
||||
╭─ tasks ──────────────────────────────────────────────────────────·focus· ╮ ╭─ the chosen task …
|
||||
```
|
||||
|
||||
It takes the title line and leaves the panels and the key hints untouched. After writing the
|
||||
`.gitignore`, pressing `R` cleared it and the title came back. There is a test for exactly this:
|
||||
one warning, height unchanged, footer preserved.
|
||||
|
||||
## lazygit, confirmed
|
||||
|
||||
`lazygit 0.66.2`, with the code paths for `ignoreOrExcludeFile` and the menu items
|
||||
**Add to .gitignore** and **Add to .git/info/exclude**. Files panel `i` ignores the selected file
|
||||
or folder, `space` stages, `c` commits, `A` amends, `e` edits — including `.gitignore`. Paths
|
||||
only, so a pattern like `*secret*` is typed in the file. Nothing in our interface duplicates it.
|
||||
|
||||
## What is now honest that was not before
|
||||
|
||||
- Paperclip does not check that a project folder is a repository. The gate is the only early check.
|
||||
- A worktree is a checkout of a commit, so **what you commit is what the agent can see** — the
|
||||
reason the tool stops before committing instead of doing it for you.
|
||||
- **`.paperclip/` in `.gitignore` is not a preference.** Paperclip puts its worktrees inside the
|
||||
project folder; without that entry a commit puts checkouts of the repository inside itself.
|
||||
- A project's worktree policy is fixed at creation, so `local_path` cannot become `git_worktree`
|
||||
later. The tab says so; it does not pretend otherwise.
|
||||
|
||||
## Committed and pushed
|
||||
|
||||
`paperclip-harness` → `4be72da`. Go builds, `go vet` is clean, and the frame tests pass including
|
||||
the new banner test. The updated commands and the new binary are also copied to `.27` and `.51`,
|
||||
so all three machines are in step.
|
||||
|
||||
Nothing was done to `~/SECURITY`, and I have not run `office` on it. When you are ready it will ask
|
||||
you Y or N, write the `.gitignore`, and stop so you can commit it in lazygit yourself.
|
||||
|
||||
## Source paths observed
|
||||
|
||||
- `home/sam/paseo/projects/paperclip-harness/bin/ha`
|
||||
- `github.com/jesseduffield/lazygit/blob/master/docs/Custom_DiffRenderers.md`
|
||||
- `.git/info/exclude`
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 84%
|
||||
- Reason: Update component/repository context with detailed discovered structure and configuration
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-12 09:39:17 -->
|
||||
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]]
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|initializing a folder as a Paperclip project with the `office` gate]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|paperclip-harness: office creation gate + project git tab + per-screen warning banner (commit 4be72da)]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/paperclip-worktree-gitignore-coupling-and-close-readiness-in|Paperclip: worktree/`.gitignore` coupling and close-readiness integration]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-39-17|Rich discovery: with 2026-11-12 09:39:17]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
|
||||
@@ -1292,3 +1292,35 @@ Follow-up: machines can then drive each other via `office <folder> --project|--n
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|Rebuild .27 and .51 after adding ~/.local/bin to home-manager PATH]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Added ~/.local/bin to PATH on .27/.51 and committed; .13 untouched]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/nixos-fleet-path-fix-rollout-27-and-51-need-home-rebuild-13-|NixOS fleet PATH fix rollout: .27 and .51 need home rebuild, .13 already correct]]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Update (2026-11-12 09:39:17)
|
||||
|
||||
Deliverables landed in `paperclip-harness` at commit `4be72da` (pushed `f7b77f3..4be72da main -> main`):
|
||||
- `office` now never writes history: new creation gate offers Y (init repo, write `.gitignore` with `.paperclip/`, stage everything, stop) / N (create project in place, no branches/worktrees). Non-interactive invocation changes nothing and prints the three commands.
|
||||
- `--commit-all` flag: full repo + `.gitignore` + commit + project with worktrees.
|
||||
- Fixed: base branch no longer hardcoded to `main` (was breaking repos on `master`); base branch set in `policy.baseRef` and workspace `defaultRef` (previously left null).
|
||||
- `Project>project` git tab: top half runs git in the folder (repository, branch, commits, remote, changed, `.gitignore` status, worktrees, recommended actions); bottom half reuses Paperclip's `close-readiness` scan.
|
||||
- Added a repo-wide warning wrapper around each screen (not the frame) so e.g. ".paperclip/ is not in .gitignore" shows once per screen; pressing `R` rebuilds panes and clears it. New test covers exactly this (one warning, height unchanged, footer preserved).
|
||||
- `t` in project screen opens lazygit in the project folder (verified, pane then closed).
|
||||
|
||||
Validation: Go builds, `go vet` clean, frame tests incl. new banner test pass. Binary and updated commands copied to machines `.27` and `.51`. Verified live on `test-alpha` before/after fixing `.gitignore`. Nothing done to `~/SECURITY`.
|
||||
|
||||
Note: git status immediately before showed untracked `.harness/` (separate from `.paperclip/`).
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 90%
|
||||
- Reason: Concrete shipped change with commit hash, validation, and follow-ups.
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-12 09:39:17 -->
|
||||
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]]
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|initializing a folder as a Paperclip project with the `office` gate]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|paperclip-harness: office creation gate + project git tab + per-screen warning banner (commit 4be72da)]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/paperclip-worktree-gitignore-coupling-and-close-readiness-in|Paperclip: worktree/`.gitignore` coupling and close-readiness integration]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-39-17|Rich discovery: with 2026-11-12 09:39:17]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
type: observation
|
||||
id: observation.paperclip-harness.paperclip-worktree-gitignore-coupling-and-close-readiness-in
|
||||
title: Paperclip: worktree/`.gitignore` coupling and close-readiness integration
|
||||
status: active
|
||||
source_of_truth: false
|
||||
freshness: current
|
||||
last_reviewed: 2026-11-12
|
||||
tags:
|
||||
- pack/paperclip-harness
|
||||
- agent-memory/observation
|
||||
- paperclip-harness
|
||||
- git-worktrees
|
||||
- gitignore
|
||||
- lazygit
|
||||
- close-readiness
|
||||
---
|
||||
|
||||
# Paperclip: worktree/`.gitignore` coupling and close-readiness integration
|
||||
|
||||
- Paperclip does not check whether a project folder is a git repository; the `office` gate is the only early check.
|
||||
- A worktree is a checkout of a commit, so "what you commit is what the agent can see" — this is why `office` deliberately stops before committing rather than committing for the user.
|
||||
- `.paperclip/` in `.gitignore` is mandatory, not a preference: Paperclip places worktrees inside the project folder, so without the entry a commit embeds repo checkouts inside the repo.
|
||||
- A project's worktree policy is fixed at creation; `local_path` cannot be converted into `git_worktree` later. The project tab states this explicitly.
|
||||
- `Project>project` bottom section uses Paperclip's own `close-readiness` scan for phrases like "1 behind", "merged into base"; wording is Paperclip's, not independently computed.
|
||||
- lazygit 0.66.2 confirmed: `i` ignores selected file/folder, `space` stages, `c` commits, `A` amends, `e` edits; paths only (globs like `*secret*` must be typed into the file). Menu items: **Add to .gitignore** / **Add to .git/info/exclude** (modules `ignoreOrExcludeFile`).
|
||||
- Session window baseline after opening/closing a pane: 8 shell/board/work/agents/reply/new/project/repair windows plus lazygit pane.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 85%
|
||||
- Reason: Factual, evidence-backed invariants and tool behaviors a future agent needs to avoid regressions.
|
||||
|
||||
## Related
|
||||
|
||||
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-12 09:39:17 -->
|
||||
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]]
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|initializing a folder as a Paperclip project with the `office` gate]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|paperclip-harness: office creation gate + project git tab + per-screen warning banner (commit 4be72da)]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/paperclip-worktree-gitignore-coupling-and-close-readiness-in|Paperclip: worktree/`.gitignore` coupling and close-readiness integration]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-39-17|Rich discovery: with 2026-11-12 09:39:17]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
@@ -6530,3 +6530,159 @@ Say go and I will build it in this order: the creation gate (with `defaultRef` a
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-git-repo-2026-11-12-09-24-20|Rich discovery: git_repo 2026-11-12 09:24:20]]
|
||||
- context: [[packs/paperclip-harness/20-context/home|git_repo]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|git_repo repository structure and configuration patterns]]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Update (2026-11-12 09:39:17)
|
||||
|
||||
## Structural/configuration observations (2026-11-12 09:39:17)
|
||||
|
||||
Built, verified on screen, and pushed. Here is what exists now.
|
||||
|
||||
## The gate: `office` never writes history
|
||||
|
||||
Answer **Y** and it stops before committing, deliberately:
|
||||
|
||||
```
|
||||
/tmp/gate1 is not a git repository, so a task cannot have its own branch and worktree.
|
||||
without one, two tasks can change the same files at once, and there
|
||||
is no diff to review and nothing to merge.
|
||||
|
||||
Y make the repository, write .gitignore with .paperclip/, stage everything,
|
||||
and stop. You commit it in lazygit, then run this command again.
|
||||
N create the project in place instead: no branches, no diff, nothing to merge.
|
||||
```
|
||||
|
||||
Verified by running it three ways:
|
||||
|
||||
| input | result |
|
||||
|---|---|
|
||||
| no repo, no terminal, no flag | changes nothing, prints the three commands |
|
||||
| `Y` on a repo with no commit | `wrote .gitignore, ignoring .paperclip/`, staged `.gitignore` and the file, **no commit**, **no project created** |
|
||||
| `--commit-all` | repo, `.gitignore`, commit, then the project is created with worktrees |
|
||||
|
||||
After the staged run, the folder looked like this — which is exactly the state you want to open lazygit in:
|
||||
|
||||
```
|
||||
$ git status --short
|
||||
A .gitignore
|
||||
A a.md
|
||||
$ cat .gitignore
|
||||
# Paperclip puts each task's worktree inside this folder, at .paperclip/.
|
||||
# It is a checkout of this repository, so it must never be committed.
|
||||
.paperclip/
|
||||
|
||||
# Add your own patterns below, one per line. For example:
|
||||
# evidence/
|
||||
# .env
|
||||
# *secret*
|
||||
```
|
||||
|
||||
Two things I fixed while in there, both real faults:
|
||||
|
||||
- the base branch was hardcoded to `main` — a repository on `master` would have been given a policy pointing at a branch that does not exist;
|
||||
- the base branch is now set in both places Paperclip keeps it: the policy's `baseRef` and the workspace's `defaultRef`, which we had been leaving `null`.
|
||||
|
||||
## The `git` tab: live on screen in `Project>project`
|
||||
|
||||
Before fixing `.gitignore` in `test-alpha`:
|
||||
|
||||
```
|
||||
THIS FOLDER
|
||||
repository yes · branch main
|
||||
commits 3
|
||||
remote none — not needed for worktrees
|
||||
changed 0 staged · 0 unstaged · 2 untracked
|
||||
.gitignore missing — .paperclip/ must be in it
|
||||
|
||||
worktrees isolated_workspace · git_worktree · base main
|
||||
|
||||
WHAT TO DO
|
||||
.paperclip/ is not ignored: a commit would swallow every worktree.
|
||||
t lazygit · select .gitignore · e to edit · add .paperclip/
|
||||
Then run R rebuild so every pane is built against the new state.
|
||||
|
||||
WORKTREES, AS PAPERCLIP SEES THEM
|
||||
AUD-172-add-a-status-md-with-one-line clean · 1 behind · merged into base
|
||||
This workspace is 1 commit behind main.
|
||||
```
|
||||
|
||||
and after:
|
||||
|
||||
```
|
||||
.gitignore .paperclip/
|
||||
|
||||
worktrees isolated_workspace · git_worktree · base main
|
||||
|
||||
WORKTREES, AS PAPERCLIP SEES THEM
|
||||
AUD-172-add-a-status-md-with-one-line clean · 1 behind · merged into base
|
||||
```
|
||||
|
||||
The top half is git run in the folder. The bottom half is Paperclip's own
|
||||
`close-readiness` scan, so "1 behind", "merged into base" and the warning text are Paperclip's
|
||||
words, not a second opinion of mine.
|
||||
|
||||
`t` opens lazygit in the project folder. I pressed it, saw lazygit open on the right repository,
|
||||
and closed the pane again — the session is back to eight windows.
|
||||
|
||||
## The warning line: once per screen, not once per panel
|
||||
|
||||
My first attempt put it in the frame, which the board draws three times side by side, so the
|
||||
warning appeared three times on one row. It is now a wrapper around each screen:
|
||||
|
||||
```
|
||||
commit will swallow the worktrees — .paperclip/ is not in .gitignore · Project>project → git
|
||||
╭─ tasks ──────────────────────────────────────────────────────────·focus· ╮ ╭─ the chosen task …
|
||||
```
|
||||
|
||||
It takes the title line and leaves the panels and the key hints untouched. After writing the
|
||||
`.gitignore`, pressing `R` cleared it and the title came back. There is a test for exactly this:
|
||||
one warning, height unchanged, footer preserved.
|
||||
|
||||
## lazygit, confirmed
|
||||
|
||||
`lazygit 0.66.2`, with the code paths for `ignoreOrExcludeFile` and the menu items
|
||||
**Add to .gitignore** and **Add to .git/info/exclude**. Files panel `i` ignores the selected file
|
||||
or folder, `space` stages, `c` commits, `A` amends, `e` edits — including `.gitignore`. Paths
|
||||
only, so a pattern like `*secret*` is typed in the file. Nothing in our interface duplicates it.
|
||||
|
||||
## What is now honest that was not before
|
||||
|
||||
- Paperclip does not check that a project folder is a repository. The gate is the only early check.
|
||||
- A worktree is a checkout of a commit, so **what you commit is what the agent can see** — the
|
||||
reason the tool stops before committing instead of doing it for you.
|
||||
- **`.paperclip/` in `.gitignore` is not a preference.** Paperclip puts its worktrees inside the
|
||||
project folder; without that entry a commit puts checkouts of the repository inside itself.
|
||||
- A project's worktree policy is fixed at creation, so `local_path` cannot become `git_worktree`
|
||||
later. The tab says so; it does not pretend otherwise.
|
||||
|
||||
## Committed and pushed
|
||||
|
||||
`paperclip-harness` → `4be72da`. Go builds, `go vet` is clean, and the frame tests pass including
|
||||
the new banner test. The updated commands and the new binary are also copied to `.27` and `.51`,
|
||||
so all three machines are in step.
|
||||
|
||||
Nothing was done to `~/SECURITY`, and I have not run `office` on it. When you are ready it will ask
|
||||
you Y or N, write the `.gitignore`, and stop so you can commit it in lazygit yourself.
|
||||
|
||||
## Source paths observed
|
||||
|
||||
- `home/sam/paseo/projects/paperclip-harness/bin/ha`
|
||||
- `github.com/jesseduffield/lazygit/blob/master/docs/Custom_DiffRenderers.md`
|
||||
- `.git/info/exclude`
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 82%
|
||||
- Reason: Captured factual repo structure/configuration patterns from rich discovery
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-12 09:39:17 -->
|
||||
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]]
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|initializing a folder as a Paperclip project with the `office` gate]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|paperclip-harness: office creation gate + project git tab + per-screen warning banner (commit 4be72da)]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/paperclip-worktree-gitignore-coupling-and-close-readiness-in|Paperclip: worktree/`.gitignore` coupling and close-readiness integration]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-39-17|Rich discovery: with 2026-11-12 09:39:17]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
|
||||
@@ -2104,3 +2104,42 @@ tuios[226383]: a daemon is already running (PID 1603888). Stop it with 'tuios ki
|
||||
- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-multi-machine-setup-program-delivery-git-w|Paperclip harness multi-machine setup: program delivery, git worktrees vs local_path, env var inheritance …[truncated]…]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-cannot-2026-11-12-06-28-28|Rich discovery: cannot 2026-11-12 06:28:28]]
|
||||
- context: [[packs/paperclip-harness/20-context/home|cannot]]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Update (2026-11-12 09:39:17)
|
||||
|
||||
Trigger: run `office` (or equivalent) against a folder that is not yet a git repository, to make it a Paperclip project with worktrees.
|
||||
|
||||
Prerequisites: lazygit installed (0.66.2 observed) for the staging step; terminal available for the interactive prompt; `office` binary copied to the host (`bin/ha` on paperclip-harness machines).
|
||||
|
||||
Step-by-step:
|
||||
1. Run `office` in the target folder. Non-interactive / no-TTY / no flag: it changes nothing and prints the three commands.
|
||||
2. Choose `Y` (interactive). Result: writes `.gitignore` containing `.paperclip/` plus commented examples (`evidence/`, `.env`, `*secret*`), stages all files (`.gitignore` + content), then STOPS — no commit, no project created. Output: `wrote .gitignore, ignoring .paperclip/`.
|
||||
3. Inspect with `git status --short` (expect `A .gitignore`, `A a.md`, ...).
|
||||
4. Open lazygit (`t` in project screen, or directly) so the user reviews and commits manually.
|
||||
5. Re-run `office` to create the project with branches/worktrees.
|
||||
|
||||
Alternative single-shot: `office --commit-all` creates the repo, `.gitignore`, makes the commit, and creates the project with worktrees in one go.
|
||||
|
||||
Safe/dangerous: Never commit without `.paperclip/` in `.gitignore` — a commit will embed worktree checkouts of the repo. `--commit-all` writes history automatically; use only when the user has agreed.
|
||||
|
||||
Validation/troubleshooting: After commit, `Project>project` git tab should show `.gitignore .paperclip/` with no warning banner, and `R` clears the banner. If banner persists: `.paperclip/` missing from `.gitignore`; edit via lazygit (`select .gitignore · e`) then `R` rebuild.
|
||||
|
||||
Follow-up observed: `~/SECURITY` was intentionally not touched; when run it will present the Y/N gate and stop after writing `.gitignore`.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 90%
|
||||
- Reason: Repeatable procedure verified three ways with explicit safe/dangerous steps.
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-12 09:39:17 -->
|
||||
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]]
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|initializing a folder as a Paperclip project with the `office` gate]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|paperclip-harness: office creation gate + project git tab + per-screen warning banner (commit 4be72da)]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/paperclip-worktree-gitignore-coupling-and-close-readiness-in|Paperclip: worktree/`.gitignore` coupling and close-readiness integration]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-39-17|Rich discovery: with 2026-11-12 09:39:17]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
type: session
|
||||
id: session.paperclip-harness.rich-discovery-with-2026-11-12-09-39-17
|
||||
title: Rich discovery: with 2026-11-12 09:39:17
|
||||
status: active
|
||||
source_of_truth: false
|
||||
freshness: current
|
||||
last_reviewed: 2026-11-12
|
||||
tags:
|
||||
- pack/paperclip-harness
|
||||
- agent-memory/session
|
||||
- autolearn
|
||||
- rich-discovery
|
||||
- repo/with
|
||||
---
|
||||
|
||||
# Rich discovery: with 2026-11-12 09:39:17
|
||||
|
||||
Sanitized rich discovery snapshot captured automatically from the completed turn.
|
||||
|
||||
|
||||
## Source paths observed
|
||||
|
||||
- `home/sam/paseo/projects/paperclip-harness/bin/ha`
|
||||
- `github.com/jesseduffield/lazygit/blob/master/docs/Custom_DiffRenderers.md`
|
||||
- `.git/info/exclude`
|
||||
|
||||
## Final answer snapshot
|
||||
|
||||
Built, verified on screen, and pushed. Here is what exists now.
|
||||
|
||||
## The gate: `office` never writes history
|
||||
|
||||
Answer **Y** and it stops before committing, deliberately:
|
||||
|
||||
```
|
||||
/tmp/gate1 is not a git repository, so a task cannot have its own branch and worktree.
|
||||
without one, two tasks can change the same files at once, and there
|
||||
is no diff to review and nothing to merge.
|
||||
|
||||
Y make the repository, write .gitignore with .paperclip/, stage everything,
|
||||
and stop. You commit it in lazygit, then run this command again.
|
||||
N create the project in place instead: no branches, no diff, nothing to merge.
|
||||
```
|
||||
|
||||
Verified by running it three ways:
|
||||
|
||||
| input | result |
|
||||
|---|---|
|
||||
| no repo, no terminal, no flag | changes nothing, prints the three commands |
|
||||
| `Y` on a repo with no commit | `wrote .gitignore, ignoring .paperclip/`, staged `.gitignore` and the file, **no commit**, **no project created** |
|
||||
| `--commit-all` | repo, `.gitignore`, commit, then the project is created with worktrees |
|
||||
|
||||
After the staged run, the folder looked like this — which is exactly the state you want to open lazygit in:
|
||||
|
||||
```
|
||||
$ git status --short
|
||||
A .gitignore
|
||||
A a.md
|
||||
$ cat .gitignore
|
||||
# Paperclip puts each task's worktree inside this folder, at .paperclip/.
|
||||
# It is a checkout of this repository, so it must never be committed.
|
||||
.paperclip/
|
||||
|
||||
# Add your own patterns below, one per line. For example:
|
||||
# evidence/
|
||||
# .env
|
||||
# *secret*
|
||||
```
|
||||
|
||||
Two things I fixed while in there, both real faults:
|
||||
|
||||
- the base branch was hardcoded to `main` — a repository on `master` would have been given a policy pointing at a branch that does not exist;
|
||||
- the base branch is now set in both places Paperclip keeps it: the policy's `baseRef` and the workspace's `defaultRef`, which we had been leaving `null`.
|
||||
|
||||
## The `git` tab: live on screen in `Project>project`
|
||||
|
||||
Before fixing `.gitignore` in `test-alpha`:
|
||||
|
||||
```
|
||||
THIS FOLDER
|
||||
repository yes · branch main
|
||||
commits 3
|
||||
remote none — not needed for worktrees
|
||||
changed 0 staged · 0 unstaged · 2 untracked
|
||||
.gitignore missing — .paperclip/ must be in it
|
||||
|
||||
worktrees isolated_workspace · git_worktree · base main
|
||||
|
||||
WHAT TO DO
|
||||
.paperclip/ is not ignored: a commit would swallow every worktree.
|
||||
t lazygit · select .gitignore · e to edit · add .paperclip/
|
||||
Then run R rebuild so every pane is built against the new state.
|
||||
|
||||
WORKTREES, AS PAPERCLIP SEES THEM
|
||||
AUD-172-add-a-status-md-with-one-line clean · 1 behind · merged into base
|
||||
This workspace is 1 commit behind main.
|
||||
```
|
||||
|
||||
and after:
|
||||
|
||||
```
|
||||
.gitignore .paperclip/
|
||||
|
||||
worktrees isolated_workspace · git_worktree · base main
|
||||
|
||||
WORKTREES, AS PAPERCLIP SEES THEM
|
||||
AUD-172-add-a-status-md-with-one-line clean · 1 behind · merged into base
|
||||
```
|
||||
|
||||
The top half is git run in the folder. The bottom half is Paperclip's own
|
||||
`close-readiness` scan, so "1 behind", "merged into base" and the warning text are Paperclip's
|
||||
words, not a second opinion of mine.
|
||||
|
||||
`t` opens lazygit in the project folder. I pressed it, saw lazygit open on the right repository,
|
||||
and closed the pane again — the session is back to eight windows.
|
||||
|
||||
## The warning line: once per screen, not once per panel
|
||||
|
||||
My first attempt put it in the frame, which the board draws three times side by side, so the
|
||||
warning appeared three times on one row. It is now a wrapper around each screen:
|
||||
|
||||
```
|
||||
commit will swallow the worktrees — .paperclip/ is not in .gitignore · Project>project → git
|
||||
╭─ tasks ──────────────────────────────────────────────────────────·focus· ╮ ╭─ the chosen task …
|
||||
```
|
||||
|
||||
It takes the title line and leaves the panels and the key hints untouched. After writing the
|
||||
`.gitignore`, pressing `R` cleared it and the title came back. There is a test for exactly this:
|
||||
one warning, height unchanged, footer preserved.
|
||||
|
||||
## lazygit, confirmed
|
||||
|
||||
`lazygit 0.66.2`, with the code paths for `ignoreOrExcludeFile` and the menu items
|
||||
**Add to .gitignore** and **Add to .git/info/exclude**. Files panel `i` ignores the selected file
|
||||
or folder, `space` stages, `c` commits, `A` amends, `e` edits — including `.gitignore`. Paths
|
||||
only, so a pattern like `*secret*` is typed in the file. Nothing in our interface duplicates it.
|
||||
|
||||
## What is now honest that was not before
|
||||
|
||||
- Paperclip does not check that a project folder is a repository. The gate is the only early check.
|
||||
- A worktree is a checkout of a commit, so **what you commit is what the agent can see** — the
|
||||
reason the tool stops before committing instead of doing it for you.
|
||||
- **`.paperclip/` in `.gitignore` is not a preference.** Paperclip puts its worktrees inside the
|
||||
project folder; without that entry a commit puts checkouts of the repository inside itself.
|
||||
- A project's worktree policy is fixed at creation, so `local_path` cannot become `git_worktree`
|
||||
later. The tab says so; it does not pretend otherwise.
|
||||
|
||||
## Committed and pushed
|
||||
|
||||
`paperclip-harness` → `4be72da`. Go builds, `go vet` is clean, and the frame tests pass including
|
||||
the new banner test. The updated commands and the new binary are also copied to `.27` and `.51`,
|
||||
so all three machines are in step.
|
||||
|
||||
Nothing was done to `~/SECURITY`, and I have not run `office` on it. When you are ready it will ask
|
||||
you Y or N, write the `.gitignore`, and stop so you can commit it in lazygit yourself.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 86%
|
||||
- Reason: Preserve full sanitized discovery detail from a rich investigation turn
|
||||
|
||||
## Related
|
||||
|
||||
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-12 09:39:17 -->
|
||||
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]]
|
||||
- runbook: [[packs/paperclip-harness/70-runbooks/tmp-deployment-operations|initializing a folder as a Paperclip project with the `office` gate]]
|
||||
- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|paperclip-harness: office creation gate + project git tab + per-screen warning banner (commit 4be72da)]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/paperclip-worktree-gitignore-coupling-and-close-readiness-in|Paperclip: worktree/`.gitignore` coupling and close-readiness integration]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-with-2026-11-12-09-39-17|Rich discovery: with 2026-11-12 09:39:17]]
|
||||
- context: [[packs/paperclip-harness/20-context/with|with]]
|
||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|with repository structure and configuration patterns]]
|
||||
BIN
bin/harness
BIN
bin/harness
Binary file not shown.
63
bin/office
63
bin/office
@@ -273,9 +273,9 @@ def run_on_remote(host, folder, opts):
|
||||
for flag, value in (("--project", opts["project"]), ("--session", opts["session"]), ("--new", opts["new"])):
|
||||
if value:
|
||||
inner += [flag, value]
|
||||
guard_machine(folder, opts)
|
||||
ready = prepare_git(folder, opts)
|
||||
|
||||
for flag in ("--git-init", "--commit-all", "--anywhere"):
|
||||
if opts.get(flag[2:].replace("-", "_")):
|
||||
inner.append(flag)
|
||||
if opts["dry"]:
|
||||
inner.append("--dry")
|
||||
# "$HOME" must stay unquoted so the other machine's shell expands it; every
|
||||
@@ -283,8 +283,16 @@ def run_on_remote(host, folder, opts):
|
||||
cmd = 'exec "$HOME/.local/share/paperclip-harness/bin/office" ' + \
|
||||
" ".join(shlex.quote(a) for a in inner)
|
||||
h.say("on %s: %s" % (host, cmd))
|
||||
out = subprocess.run(["ssh", "-o", "StrictHostKeyChecking=accept-new", addr, cmd],
|
||||
capture_output=True, text=True, timeout=600)
|
||||
# A question is asked on the other machine when the folder cannot hold a
|
||||
# worktree, so give it a terminal when this one has one: without it, ssh
|
||||
# hands it a pipe and it has to refuse instead of asking.
|
||||
ssh_args = ["ssh", "-o", "StrictHostKeyChecking=accept-new"]
|
||||
interactive = sys.stdin.isatty()
|
||||
if interactive:
|
||||
ssh_args.append("-t")
|
||||
out = subprocess.run(ssh_args + [addr, cmd], timeout=600) if interactive else \
|
||||
subprocess.run(ssh_args + [addr, cmd], capture_output=True, text=True, timeout=600)
|
||||
if not interactive:
|
||||
sys.stdout.write(out.stdout)
|
||||
if out.returncode != 0:
|
||||
raise h.Refused(3, "the other machine refused: %s" % (out.stderr or out.stdout).strip()[:300])
|
||||
@@ -445,6 +453,20 @@ def guard_machine(folder, opts):
|
||||
h.say(("warning: " if i == 0 else " ") + line)
|
||||
|
||||
|
||||
def repeat_command(folder, opts):
|
||||
"""The same command again, spelled out, with the arguments that were used."""
|
||||
parts = ["office", folder]
|
||||
if opts.get("new"):
|
||||
parts += ["--new", '"%s"' % opts["new"]]
|
||||
elif opts.get("project"):
|
||||
parts += ["--project", '"%s"' % opts["project"]]
|
||||
if opts.get("session"):
|
||||
parts += ["--session", opts["session"]]
|
||||
if opts.get("host"):
|
||||
parts += ["--host", opts["host"]]
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def prepare_git(folder, opts):
|
||||
"""Get the folder as far as it can go toward a worktree per task.
|
||||
|
||||
@@ -483,9 +505,17 @@ def prepare_git(folder, opts):
|
||||
h.say(" git add -A && git commit -m 'Initial commit'")
|
||||
h.say("or pass --commit-all to let this command do all of it.")
|
||||
raise h.Refused(2, "no repository and no terminal to ask in")
|
||||
h.say(" Y make the repository, write .gitignore with .paperclip/,")
|
||||
h.say(" stage everything, and stop. You commit it in lazygit, then")
|
||||
h.say(" run this command again to create the project with worktrees.")
|
||||
h.say(" Y make the repository now, then three steps:")
|
||||
h.say("")
|
||||
h.say(" 1. PUT INTO .gitignore EVERY FILE YOU DO NOT WANT COMMITTED.")
|
||||
h.say(" A file there is invisible to the agent, which is usually what")
|
||||
h.say(" you want for evidence folders, notes with credentials, and")
|
||||
h.say(" .paperclip/, which is written there for you.")
|
||||
h.say(" 2. commit what is left, in lazygit: stage with space, commit with c.")
|
||||
h.say(" 3. run this same command again to create the project with worktrees:")
|
||||
h.say("")
|
||||
h.say(" %s" % repeat_command(folder, opts))
|
||||
h.say("")
|
||||
h.say(" N create the project in place instead: no branches, no diff,")
|
||||
h.say(" nothing to merge.")
|
||||
h.say("")
|
||||
@@ -514,14 +544,18 @@ def prepare_git(folder, opts):
|
||||
return "ready"
|
||||
|
||||
h.say("")
|
||||
h.say(" Everything is staged and nothing is committed. Check what git is about")
|
||||
h.say(" to take, and commit it:")
|
||||
h.say(" Everything is staged and nothing is committed. What you commit is what")
|
||||
h.say(" the agent will be able to see, so check it first:")
|
||||
h.say("")
|
||||
h.say(" cd %s && lazygit" % folder)
|
||||
h.say(" space stage or unstage a file i ignore a file or folder")
|
||||
h.say("")
|
||||
h.say(" i put the selected file or folder into .gitignore")
|
||||
h.say(" space stage or unstage e edit a file, .gitignore included")
|
||||
h.say(" c commit ? every key")
|
||||
h.say("")
|
||||
h.say(" Then run this command again. It will create the project with worktrees.")
|
||||
h.say(" Then repeat this command to create the project with worktrees:")
|
||||
h.say("")
|
||||
h.say(" %s" % repeat_command(folder, opts))
|
||||
return "staged"
|
||||
|
||||
|
||||
@@ -586,6 +620,8 @@ def main():
|
||||
else:
|
||||
return run_on_remote(opts["remote"], folder, opts)
|
||||
|
||||
# Only now: a folder on another machine has already been handed over, and
|
||||
# the machine that owns it does these two checks itself.
|
||||
guard_machine(folder, opts)
|
||||
ready = prepare_git(folder, opts)
|
||||
|
||||
@@ -646,9 +682,6 @@ def main():
|
||||
"watching": watching,
|
||||
}
|
||||
|
||||
guard_machine(folder, opts)
|
||||
ready = prepare_git(folder, opts)
|
||||
|
||||
if opts["dry"]:
|
||||
h.say("would write %s" % pj.path(folder))
|
||||
h.say("would build pages: shell, tree, reply, log, new")
|
||||
|
||||
43
tui/board.go
43
tui/board.go
@@ -183,28 +183,18 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
if m.tools {
|
||||
switch msg.String() {
|
||||
case "esc", "q", "t":
|
||||
key := msg.String()
|
||||
if key == "esc" || key == "q" || key == "t" {
|
||||
m.tools = false
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
m.toolAt = max(0, m.toolAt-1)
|
||||
return m, nil
|
||||
case "down", "j":
|
||||
m.toolAt = min(len(tools)-1, m.toolAt+1)
|
||||
return m, nil
|
||||
case "g":
|
||||
m.toolAt = 0
|
||||
return m, nil
|
||||
case "G":
|
||||
m.toolAt = len(tools) - 1
|
||||
return m, nil
|
||||
case "enter":
|
||||
t := tools[m.toolAt]
|
||||
}
|
||||
if consumed, chosen := toolPickerKeys(key, &m.toolAt); consumed {
|
||||
if chosen != "" {
|
||||
m.tools = false
|
||||
if err := openTool(m.folder, t.name); err != nil {
|
||||
if err := openTool(m.folder, chosen); err != nil {
|
||||
m.err = err
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
@@ -247,6 +237,12 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "t":
|
||||
m.tools = true
|
||||
return m, nil
|
||||
case "l":
|
||||
// lazygit in the project folder, the same key as on the other panes.
|
||||
if err := openTool(m.folder, "lazygit"); err != nil {
|
||||
m.err = err
|
||||
}
|
||||
return m, nil
|
||||
case "/":
|
||||
m.typing = true
|
||||
return m, nil
|
||||
@@ -337,7 +333,7 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.needsTab--
|
||||
}
|
||||
return m, nil
|
||||
case "]", "l":
|
||||
case "]":
|
||||
if m.focus == focusNeeds && m.needsTab < 1 {
|
||||
m.needsTab++
|
||||
}
|
||||
@@ -406,15 +402,7 @@ func (m boardModel) View() string {
|
||||
}, m.width, m.height)
|
||||
}
|
||||
if m.tools {
|
||||
entries := make([][2]string, 0, len(tools))
|
||||
for i, t := range tools {
|
||||
mark := " "
|
||||
if i == m.toolAt {
|
||||
mark = "▸ "
|
||||
}
|
||||
entries = append(entries, [2]string{mark + t.name, t.about})
|
||||
}
|
||||
return helpOverlay("open a tool pane here", entries, m.width, m.height)
|
||||
return toolMenu(m.toolAt, m.width, m.height)
|
||||
}
|
||||
if m.showHelp {
|
||||
return helpOverlay("keys on this panel", [][2]string{
|
||||
@@ -519,6 +507,7 @@ func (m boardModel) footerLine() string {
|
||||
keyHint("u", "unblock"),
|
||||
keyHint("c", "cancel"),
|
||||
keyHint("t", "tools"),
|
||||
keyHint("l", "lazygit"),
|
||||
keyHint("/", "filter"),
|
||||
keyHint("?", "keys"),
|
||||
keyHint("tab", "panel"),
|
||||
|
||||
@@ -54,6 +54,8 @@ type projectModel struct {
|
||||
|
||||
git gitState
|
||||
wts []worktreeRow
|
||||
tools bool
|
||||
toolAt int
|
||||
|
||||
tab int
|
||||
scroll int
|
||||
@@ -168,6 +170,25 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(m.load(), tickEvery(10*time.Second))
|
||||
|
||||
case tea.KeyMsg:
|
||||
// The tool chooser owns every key while it is open, before the
|
||||
// confirmation dialogs and before the tabs.
|
||||
if m.tools {
|
||||
key := msg.String()
|
||||
if key == "esc" || key == "q" || key == "t" {
|
||||
m.tools = false
|
||||
return m, nil
|
||||
}
|
||||
if consumed, chosen := toolPickerKeys(key, &m.toolAt); consumed {
|
||||
if chosen != "" {
|
||||
m.tools = false
|
||||
if err := openTool(m.folder, chosen); err != nil {
|
||||
m.err = err
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.confirmSwitch {
|
||||
switch msg.String() {
|
||||
case "y":
|
||||
@@ -204,8 +225,11 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "r":
|
||||
return m, m.load()
|
||||
case "t":
|
||||
// lazygit, in the project folder: it is the tool that can stage,
|
||||
// exclude and commit. Nothing here reimplements it.
|
||||
m.tools = true
|
||||
return m, nil
|
||||
case "l":
|
||||
// lazygit in the project folder: the tool that stages, excludes and
|
||||
// commits. `t` is the chooser, as it is on every other pane.
|
||||
if err := openTool(m.folder, "lazygit"); err != nil {
|
||||
m.err = err
|
||||
}
|
||||
@@ -224,13 +248,13 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
case "(", "[", "h":
|
||||
case "(", "[":
|
||||
if m.tab > 0 {
|
||||
m.tab--
|
||||
m.scroll = 0
|
||||
}
|
||||
return m, nil
|
||||
case ")", "]", "l", "tab":
|
||||
case ")", "]", "tab":
|
||||
if m.tab < len(projectTabs)-1 {
|
||||
m.tab++
|
||||
m.scroll = 0
|
||||
@@ -289,10 +313,14 @@ func (m projectModel) View() string {
|
||||
{"esc or n", "stay here"},
|
||||
}, m.width, m.height)
|
||||
}
|
||||
if m.tools {
|
||||
return toolMenu(m.toolAt, m.width, m.height)
|
||||
}
|
||||
if m.showHelp {
|
||||
return helpOverlay("the project", [][2]string{
|
||||
{"( )", "overview, git, facts, costs, settings, projects"},
|
||||
{"t", "open lazygit in this folder"},
|
||||
{"t", "open a tool pane here (lazygit, yazi, nvim, …)"},
|
||||
{"l", "lazygit, straight away"},
|
||||
{"[ ] h l", "previous and next tab"},
|
||||
{"↑ ↓ j k", "scroll"},
|
||||
{"r", "read again"},
|
||||
@@ -344,7 +372,8 @@ func (m projectModel) View() string {
|
||||
|
||||
footer := hintLine([]string{
|
||||
keyHint("( )", "tab"),
|
||||
keyHint("t", "lazygit"),
|
||||
keyHint("t", "tools"),
|
||||
keyHint("l", "lazygit"),
|
||||
keyHint("↑↓", "scroll"),
|
||||
keyHint("r", "refresh"),
|
||||
keyHint("R", "rebuild"),
|
||||
|
||||
@@ -215,3 +215,40 @@ func switchWorkspace(name string) error {
|
||||
}
|
||||
return fmt.Errorf("no workspace called %q", name)
|
||||
}
|
||||
|
||||
// toolMenu is the chooser used by every pane that offers `t`. It lives here so
|
||||
// the board and the project screen cannot drift apart in how they list the same
|
||||
// tools.
|
||||
func toolMenu(at, width, height int) string {
|
||||
entries := make([][2]string, 0, len(tools))
|
||||
for i, t := range tools {
|
||||
mark := " "
|
||||
if i == at {
|
||||
mark = "▸ "
|
||||
}
|
||||
entries = append(entries, [2]string{mark + t.name, t.about})
|
||||
}
|
||||
return helpOverlay("open a tool pane here", entries, width, height)
|
||||
}
|
||||
|
||||
// toolPickerKeys handles the chooser and says whether the key was consumed.
|
||||
// `selected` is the tool to open, or empty.
|
||||
func toolPickerKeys(key string, at *int) (consumed bool, selected string) {
|
||||
switch key {
|
||||
case "up", "k":
|
||||
*at = max(0, *at-1)
|
||||
return true, ""
|
||||
case "down", "j":
|
||||
*at = min(len(tools)-1, *at+1)
|
||||
return true, ""
|
||||
case "g":
|
||||
*at = 0
|
||||
return true, ""
|
||||
case "G":
|
||||
*at = len(tools) - 1
|
||||
return true, ""
|
||||
case "enter":
|
||||
return true, tools[*at].name
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user