(&json) {
+ if matches!(event, TerminalEvent::Exit { .. }) {
+ observed_count.store(observed_manager.count(&observed_path), Ordering::Release);
+ }
let _ = send.send(event);
}
}
Ok(())
});
- let manager = TerminalManager::default();
let handle = manager
.create(
dir.path().to_string_lossy().into_owned(),
@@ -850,6 +853,7 @@ mod tests {
);
}
assert!(String::from_utf8_lossy(&output).contains("strand-terminal"));
+ assert_eq!(count_at_exit.load(Ordering::Acquire), 0);
assert_eq!(manager.count(&dir.path().to_string_lossy()), 0);
manager.close(&handle.id).unwrap(); // natural exit made close idempotent
}
diff --git a/crates/strand-tauri/src/user_actions.rs b/crates/strand-tauri/src/user_actions.rs
index e747361..0dbc69c 100644
--- a/crates/strand-tauri/src/user_actions.rs
+++ b/crates/strand-tauri/src/user_actions.rs
@@ -234,6 +234,9 @@ mod tests {
child.stdout(Stdio::inherit()).stderr(Stdio::inherit());
child.spawn().unwrap();
println!("spawned child");
+ use std::io::Write;
+ std::io::stdout().flush().unwrap();
+ std::fs::write(std::env::var("STRAND_ACTION_READY").unwrap(), "ready").unwrap();
std::thread::sleep(Duration::from_secs(60));
}
"parent-exit" => {
@@ -324,15 +327,23 @@ mod tests {
fn cancellation_stops_descendants_and_pre_cancel_never_spawns() {
let dir = tempfile::tempdir().unwrap();
let marker = dir.path().join("marker");
+ let ready = dir.path().join("ready");
let mut command = child_command("descendant");
command.env("STRAND_ACTION_MARKER", &marker);
+ command.env("STRAND_ACTION_READY", &ready);
let cancel = AiCancelHandle::new();
let trigger = cancel.clone();
- std::thread::spawn(move || {
- std::thread::sleep(Duration::from_millis(700));
+ let watcher = std::thread::spawn(move || {
+ let deadline = Instant::now() + Duration::from_secs(10);
+ while !ready.exists() && Instant::now() < deadline {
+ std::thread::sleep(Duration::from_millis(10));
+ }
+ let started = ready.exists();
trigger.cancel();
+ started
});
- let output = capture_command(command, &cancel, Duration::from_secs(10)).unwrap();
+ let output = capture_command(command, &cancel, Duration::from_secs(15)).unwrap();
+ assert!(watcher.join().unwrap(), "child did not reach the cancellation checkpoint: {}", output.stderr);
assert_eq!(output.status, "cancelled");
assert!(output.stdout.contains("spawned child"));
std::thread::sleep(Duration::from_secs(2));
diff --git a/docs/learnings.md b/docs/learnings.md
index 750a52f..9d57a31 100644
--- a/docs/learnings.md
+++ b/docs/learnings.md
@@ -1,5 +1,31 @@
# Learnings
+## File actions must preserve literal entries and threatened bytes (2026-09-29)
+
+Audit probes showed that libgit2 checkout/reset pathspecs expand selected
+filenames such as `[id].tsx` to unrelated `i.tsx`. An exact file action needs
+literal matching; `--` only separates options and does not by itself disable
+Git pathspec magic. Keep bulk operations batched when repairing this.
+
+A hard reset can overwrite untracked or ignored entries that obstruct its
+target tree. A tracked-only dirty check/snapshot cannot promise recovery for
+those bytes. Inspect target collisions before destructive dispatch.
+
+Use directory-entry metadata for symlink staging: a missing referent does not
+make the link deleted. Working-tree containment alone also does not exclude
+`.git`, and direct joins for special files such as `.gitignore` bypass the
+existing symlink boundary. Regression coverage must include these cases.
+The git2 0.19 checkout binding does not expose literal-pathspec mode. Keep
+ordinary filenames on the existing in-process path; special-name batches use
+one NUL-delimited Git operation, with `--literal-pathspecs` for reset and exact
+`checkout-index --stdin` filenames for discard.
+
+macOS tests must canonicalize Unix paths used in `includeIf.gitdir`, explicitly
+put accepted test-server sockets into blocking mode, and wait for subprocess
+readiness before measuring cancellation. A timer started before spawn measures
+OS launch latency as well as cancellation. On Unix, signal an owned process
+group even when its leader exited: descendants can still own the pipes.
+
## Programmatic clipboard is native so the OS names Strand (2026-09-21)
`navigator.clipboard` in the Tauri webview is attributed to the web origin
@@ -2925,3 +2951,19 @@ and restore the ordinary build configuration after a CDP test build.
Core tests that spawn Git daemons also need process-tree cleanup: killing Git
for Windows' parent wrapper alone can leave its daemon alive and prevent the
test command from returning after all assertions pass.
+
+## Keep process identity until cleanup and refresh safety inputs (2026-09-29)
+
+On Unix, `Child::try_wait` reaps an exited process. Observe provider exit with
+`waitid(WNOWAIT)` and signal its owned group before `wait`, so PID reuse cannot
+redirect cleanup to an unrelated process. Pipe readiness alone does not hold
+that identity. Windows streaming Git needs an owned Job Object: assign while
+the process is suspended, then resume, and terminate the job even after the
+wrapper exits. Share the job wrapper across core and Tauri rather than reopen
+numeric PIDs or duplicate cleanup ownership.
+
+Hard-reset collision guards must refresh the index before trusting tracked
+membership; cached libgit2 indexes can survive external Git writes. Preserve
+sparse expansion through its existing reader. git2 0.19 `Index::get_path`
+already normalizes Windows separators through `path_to_repo_path`; do not add
+lossy string conversion to fix a raw-libgit2 issue the Rust binding handles.
diff --git a/docs/main-audit-2026-09-29.md b/docs/main-audit-2026-09-29.md
new file mode 100644
index 0000000..a7e4804
--- /dev/null
+++ b/docs/main-audit-2026-09-29.md
@@ -0,0 +1,339 @@
+# Main audit — 2026-09-29
+
+Audited `f5ed9a875adbbb70cbef98987e24b298b85ce2f1` (1.7.2), after a clean
+fast-forward of local `main` from `df612d8`. No application fixes, commits or
+pushes were made during the audit. The implementation follow-up is recorded
+below. This is a targeted correctness, safety, performance and
+backlog audit, not exhaustive certification of every feature.
+
+Environment: macOS Apple Silicon, Rust 1.92.0, Apple Git 2.54.0, Node 25.9.0,
+pnpm 9.0.0. Git LFS is absent. Frontend dependencies were synchronized with
+`pnpm install --frozen-lockfile`; no manifest or lockfile changed.
+
+## Prioritized findings
+
+P1 means fix before the next release; P2 means a concrete follow-up. Six
+findings were reproduced against the actual engine or existing tests. A05 is
+confirmed by source inspection, without an out-of-memory stress test.
+
+### A01 / P1 — File discard and unstage expand literal filenames as patterns
+
+Evidence: `crates/strand-core/src/stage.rs:137–141`, `:88`, `:170`.
+`CheckoutBuilder::path` and `reset_default` receive selected filenames as
+pathspecs without disabling wildcard matching. The UI calls these paths from
+`useRepo.discardMany` / `unstageMany`; bulk discard does not create a safety
+stash (`ui/src/stores/repo.ts:1764`).
+
+Reproduction: commit `[id].tsx` and `i.tsx`, modify both, then call
+`Repo::discard_path("[id].tsx")`. Both files revert, including the unrelated
+`i.tsx`. Stage new changes to both and call `unstage_path("[id].tsx")`:
+`git diff --cached --name-only` becomes empty. This affects valid cross-platform
+filenames, including bracketed route filenames; it is not limited to Unix `*`.
+
+Fix criterion: single and bulk actions affect exactly the selected paths,
+including `[]`, `*`, `?` and pathspec-like prefixes. Disable checkout pathspec
+matching and use an exact-path index reset strategy. Preserve the existing
+batched in-process hot path and narrow Windows fallback.
+
+### A02 / P1 — Hard reset overwrites colliding untracked content without recovery
+
+Evidence: `crates/strand-core/src/reset.rs:60–93` and
+`ui/src/views/ResetDialog.tsx:108`.
+The dirty check explicitly excludes untracked/ignored files and snapshots with
+`include_untracked=false`. Its premise that hard reset never touches untracked
+files is false when the target tree needs their paths. The dialog promises a
+safety snapshot.
+
+Reproduction: commit `collision.txt`, delete and commit it, recreate it with
+unique untracked bytes, then call `reset("HEAD~1", ResetMode::Hard)`. The bytes
+become the old committed content and `snapshot_oid` is `None`.
+
+Fix criterion: preflight collisions against the target tree and either refuse
+or preserve the threatened content before resetting. Include file/directory
+collisions and ignored data in regression coverage, with native, sparse and
+LFS dispatch coverage. Do not claim a recovery snapshot unless it covers the
+data actually overwritten; avoid an unconditional stash round trip.
+
+### A03 / P1 — Add to .gitignore follows symlinks outside the checkout
+
+Evidence: `crates/strand-core/src/ignore.rs:20–37`.
+The quick action joins `.gitignore` directly to the repository path and reads
+and rewrites it without the working-tree guard or a no-symlink check.
+
+Reproduction: point `.gitignore` at a text file in a separate temporary
+directory, then call `gitignore_add("/build")`. The operation succeeds and the
+external file gains `/build\n`. Only disposable files were used in this probe.
+
+Fix criterion: reject symlinked/nonregular ignore files and escaped resolved
+destinations before reading/writing; test both existing and dangling symlinks.
+An ordinary Ignore action must never mutate a repository-controlled external
+target. This is a file-boundary bug; no code-execution exploit was attempted.
+
+### A04 / P2 — Staging a dangling symlink silently stages nothing
+
+Evidence: `crates/strand-core/src/stage.rs:16–23` and `:50–57`.
+`Path::exists` follows the link, so a present link with an absent referent is
+classified as a deleted file. Git tracks the link itself.
+
+Reproduction on macOS: create `link -> missing-target`, call
+`stage_path("link")`; it returns `Ok(())`, but `git ls-files --stage link` is
+empty. The bulk path repeats the same existence check. For an already tracked
+link, this branch can remove its index entry instead of staging the link.
+
+Fix criterion: inspect directory-entry existence with `symlink_metadata`,
+distinguish NotFound from other I/O errors, and test new/modified dangling links
+through single and bulk staging. Preserve mode `120000` and the link text.
+
+### A05 / P2 — The 2 MB content limit does not bound the disk read
+
+Evidence: `crates/strand-core/src/file.rs:110–115`, `:259–260`.
+`file_content` calls `std::fs::read` for the entire working-tree file before
+`build_content` truncates the result to 2,000,000 bytes. Selecting a very large
+log or generated asset therefore allocates its complete size in the backend,
+even though the UI displays only a prefix or a binary notice.
+
+Fix criterion: open a regular file and perform a bounded prefix read, retaining
+correct truncated/binary/editable flags and UTF-8 boundaries. Ensure changing
+file size cannot defeat the bound. Measure memory and bytes read on a large
+fixture; frontend virtualization does not protect native allocations. Audit
+revision/blob reads separately rather than claiming they are already bounded.
+
+### A06 / P2 — Rename/move allows destinations inside .git
+
+Evidence: `crates/strand-core/src/rename.rs:30–31`, `:51–61` and
+`ui/src/views/RenameFileDialog.tsx:55–63`.
+The rename dialog accepts a full relative destination. The native checks
+enforce checkout containment but omit the administrative-path rejection used
+by file create/delete.
+
+Reproduction: `move_path("scratch.txt", ".git/audit-moved")` succeeds for an
+untracked file. It disappears from the working tree and appears inside Git
+metadata. Existing destination files are still protected against overwrite;
+this finding does not claim otherwise.
+
+Fix criterion: reject administrative source/destination components before
+creating directories or moving data, including aliases into the Git directory.
+Cover untracked files, directories and linked worktrees. Apply the guard at
+the native mutation boundary, not only in the dialog.
+
+### A07 / P2 — Missing-worktree removal fails through a macOS path alias
+
+Evidence: `crates/strand-core/src/worktree.rs:225–229`.
+Registration lookup compares literal paths or canonicalizes the entire target.
+Once the target is absent, canonicalization fails; `/var/...` no longer matches
+Git's stored `/private/var/...` spelling.
+
+Reproduction: existing test
+`worktree::tests::removal_only_skips_archive_when_the_registered_directory_is_missing`
+fails independently on this Mac at line 1411 with `not a registered worktree`
+after removing the directory. It also fails with system/global Git config
+isolated. The analogous existing-directory identity guard remains important.
+
+Fix criterion: reconcile missing target identities through their existing
+ancestors or authoritative registered identity without weakening
+archive-before-remove or different-repository checks. Retest macOS aliases,
+ordinary paths and the existing archive-failure cases.
+
+## Baseline verification and limitations (before fixes)
+
+| Check | Result |
+| --- | --- |
+| `pnpm --filter ./ui exec tsc --noEmit` | Pass |
+| `pnpm --filter ./ui test` | 99 files, 568 tests pass |
+| `pnpm build` | Pass; entry chunk 2,013.31 kB / 573.63 kB gzip; Vite large-chunk warning |
+| `cargo check -p strand-core -p strand-tauri` | Pass |
+| `pnpm release:check-security` | Pass |
+| `pnpm release:test-helper` | 9 tests pass |
+| `pnpm release:check-helper` | Live protocol-7 manifest, signature asset and three platform archives available; not a fresh cryptographic verification |
+| `cargo test -p strand-core -p strand-tauri` | Core stopped the command: 203 pass, 16 fail, 6 ignored |
+| Isolated core run below | 210 pass, 2 fail, 6 ignored, 7 LFS tests filtered |
+| Separate `cargo test -p strand-tauri` | 162 pass, 3 fail |
+| Serial cancellation subset below | 5 pass, 1 fail |
+
+Core failure breakdown: seven missing-Git-LFS prerequisites; seven fixtures
+inherited personal signing settings and failed through the signing agent; one
+conditional-identity expectation; one missing-worktree removal (A07).
+No personal Git configuration was changed. This isolated rerun removes the
+signing-environment failures, but does not certify LFS:
+
+```sh
+GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
+ cargo test -p strand-core -- --skip lfs
+cargo test -p strand-tauri cancel -- --test-threads=1
+```
+
+The conditional-identity test still expects `Conditional` and receives `Base`
+(`gitconfig.rs:206`). Its includeIf pattern uses a noncanonical temporary path;
+diagnose fixture path semantics against system Git before labeling it a
+production identity regression.
+
+Two Tauri cancellation deadline failures pass in the serial rerun. The user
+action descendant test still fails at `user_actions.rs:337`, expecting
+`spawned child` in stdout after a fixed 700 ms delay. Investigate child readiness
+and macOS capture/startup behavior; the failure alone does not prove a process
+escaped cancellation. These are open validation issues, not a green suite.
+
+The disposable engine probe exercised A01–A04 and A06 directly, with the
+following output (A01 used `[id].tsx` and `i.tsx`):
+
+```text
+DISCARD wildcard: selected="old literal", unrelated="old neighbour"
+UNSTAGE wildcard: staged files=""
+RESET collision: content="old committed", snapshot=None
+STAGE dangling symlink: result=Ok(()), index=""
+IGNORE symlink: result=Ok(()), external content="outside original\n/build\n"
+MOVE into git metadata: result=Ok(()), exists=true
+```
+
+No native UI walkthrough, packaged installer/updater cycle, live provider write,
+full dependency vulnerability scan, or current PRD performance certification
+was performed. Passing builds and tests do not establish those claims.
+
+## What Strand needs next
+
+1. **Protect local data first.** Fix A01–A03 with real-repository regressions,
+ then A04–A07. Keep each logical fix separate and preserve batched hot paths.
+2. **Make macOS verification trustworthy.** Isolate test Git config, state
+ prerequisites for Git LFS, resolve the identity/cancellation tests, and add
+ a macOS Rust test job. CI currently has Linux Rust and Windows-specific
+ jobs, but no macOS test job. Complete native terminal process-tree,
+ workspace persistence and Workbench continuity checks on macOS/Linux.
+3. **Certify current performance.** Existing TASKS already tracks first-use
+ grammar/paint, cold launch, idle memory and sustained-edit gaps. Measure the
+ exact production candidate against PRD §8, separating native reads, IPC,
+ highlighting and visible paint. The entry bundle is a lead to profile, not
+ proof of a responsiveness regression.
+4. **Close delivery evidence.** Run current macOS/GNOME/KDE install/update
+ checks and live Azure iteration/suggestion validation. Reconcile helper and
+ historical release rows: protocol 7 is available now, while protocol-6
+ backfill and old Store/SEO tasks require current external evidence. Do not
+ assume every unchecked historical row is still missing.
+5. **Then prioritize product expansion.** Plugin isolation/quotas and remote
+ install, typed Workbench context/services, SSH artifact/bootstrap and remote
+ directory browsing, CLI terminal rendering/distribution, and the proposed
+ per-run review checkpoints remain explicit incomplete families. Small UX
+ follow-ups include repository-tab reordering, truthful encoding/EOL status
+ and per-file persistence. Select these by user need after hardening.
+
+Planning cleanup is also warranted: PRD still contains historical unresolved
+licensing/pricing questions already answered in TASKS; ROADMAP's cross-cutting
+questions still label PR review as a candidate despite its implementation.
+Keep historical audit evidence, but provide one concise current release gate
+and distinguish shipped features from pending validation.
+
+
+## Implementation follow-up — 2026-09-29
+
+A01–A07 are implemented with regression tests. Historical findings and
+failed baseline runs above are retained as
+reproduction evidence, not the current implementation status.
+
+- **A01:** `needs_literal_pathspec` routes special-name batches through one
+ NUL-delimited Git command. Reset uses `--literal-pathspecs`; checkout-index
+ takes exact filenames. Ordinary batches retain their libgit2 fast path.
+ Regressions cover bracketed routes, wildcard/negation/comment characters,
+ spaces, Unix backslashes, single/bulk operations and unborn HEAD.
+- **A02:** `guard_reset_tree` checks target collisions before snapshots or any
+ reset dispatch. Directory replacement inspects threatened descendants,
+ including ignored files, without scanning unrelated dependency directories.
+ It refuses collisions; it does not attempt an implicit untracked stash.
+ Normal, sparse-index, LFS and file/directory fixtures retain bytes and Git
+ state on refusal. The dialog and guide now describe the actual coverage.
+- **A03:** Ignore checks containment and regular-file metadata, opens without
+ following symlinks, and reads/writes one handle. Missing files use create-new.
+ Existing, dangling and in-tree symlinks and directory targets are tested.
+- **A04:** `entry_exists` uses symlink metadata and propagates non-NotFound
+ errors. New and modified dangling links retain mode 120000 and target text
+ through single and bulk staging.
+- **A05:** Working-tree content reads at most 2,000,001 bytes; valid UTF-8 is
+ not split at the preview boundary. The 1 GiB sparse-file regression ran in
+ 0.10 seconds with 18,628,608 bytes maximum RSS for the test process on this
+ Mac (`/usr/bin/time -l`), not a packaged-app memory claim. Historical blob
+ materialization remains a separate path and is not certified by this test.
+- **A06:** `guard_move_metadata` rejects administrative components and resolved
+ aliases into per-worktree/common Git metadata before creating directories.
+ Tests include linked worktrees, aliases and working-tree root moves.
+- **A07:** `resolve_missing_path` canonicalizes the existing ancestor while
+ retaining the missing suffix. Existing-directory identity and recovery-archive
+ guards remain intact and their regressions pass.
+
+Validation repairs also isolate the affected test fixtures from personal
+signing/LFS settings, canonicalize Unix includeIf paths, switch accepted LFS
+mock-server sockets to blocking mode on macOS, and replace fixed cancellation
+sleeps with child-readiness checkpoints. Git LFS 3.8.0 was installed locally
+for verification, without changing global Git configuration. The Rust CI
+matrix now includes macOS as well as Linux; its hosted run is still pending.
+
+Stronger cancellation checks exposed two additional production gaps. Unix Git
+cancellation now signals the owned group even after the leader exits. Hosted
+provider commands now use Unix process groups / Windows Job Objects and stop
+helpers before joining pipes on cancellation, timeout, error and natural
+completion. The active-child and exited-parent regressions pass. Provider
+output reads remain unbounded; TASKS records that separate follow-up.
+
+Verification after fixes:
+
+- Core: 232 unit tests and 13 integration tests pass; six existing optional
+ signing/Git-flow/measurement tests remain ignored.
+- Tauri: 166 tests pass, including provider helper cleanup and readiness-based
+ cancellation tests.
+- Frontend: all 568 tests pass; TypeScript passes.
+- `cargo check -p strand-core -p strand-tauri` and clippy with `-D warnings` pass.
+- `git diff --check` passes. The audit's earlier production frontend build and
+ release-policy results remain baseline evidence; no packaged app was rebuilt
+ or released in this repair pass.
+
+The repository planning corrections are limited to evidence already available:
+PRD licensing/pricing decisions and ROADMAP's implemented PR-review surface.
+Native macOS/Linux packaged-app validation, Windows execution of the new native
+branches, production PRD performance certification, and external Store/SEO/
+older-helper publication reconciliation remain open. Product-expansion items
+in the original audit remain separate future work, not implied fixes in this
+hardening change.
+
+## PR #138 review follow-up — 2026-09-29
+
+The eight inline comments repeat four findings. Both Windows separator
+findings are false positives: git2 0.19 `Index::get_path` invokes
+`path_to_repo_path`, whose Windows branch normalizes backslashes before the
+libgit2 lookup. A cross-platform regression exercises nested tracked files and
+tracked-directory replacement without adding lossy path conversion.
+
+Two process findings are valid and repaired:
+
+- Provider success cleanup previously signaled a numeric Unix group after
+ `try_wait` reaped its leader. `provider_exited` now uses `waitid(WNOWAIT)`;
+ cleanup signals the group before `wait` releases the PID. A regression
+ proves repeated exit observations leave the child waitable, while the
+ existing helper-held-pipe test verifies cleanup still completes.
+- Windows streaming Git previously skipped tree cleanup when its leader had
+ exited. It now owns a Job Object assigned while Git is suspended, then
+ resumes the primary thread. Cancellation terminates that job regardless of
+ leader lifetime. The shared wrapper uses owned handles, including the
+ existing process handle for assignment, and is reused by Tauri runners.
+ A Windows regression reaps the leader while its helper retains stdout,
+ then verifies cancellation closes the pipe promptly.
+
+The nested reset regression exposed an additional real bug: a cached index
+could survive an external Git change. The guard now refreshes the normal index
+(and retains sparse expansion), preventing both false collision reports and
+missed untracked collisions. Both cases have regression coverage.
+
+Local follow-up verification: 234 core unit tests, 13 core integration tests,
+and 167 Tauri tests pass (414 total; six existing optional tests ignored).
+Cargo check, strict Clippy and diff whitespace checks pass. The shared Windows
+job module cross-compiles for x86_64-pc-windows-msvc; runtime verification is
+delegated to the Windows CI reset, cancellation and provider test subsets.
+The preceding PR head passed all five hosted checks; the revised head requires
+a fresh run. No frontend behavior or TypeScript code changed in this follow-up.
+
+The first revised Linux run exposed a pre-existing terminal lifecycle race:
+`pty_streams_ordered_output_then_exit` received Exit while the session count
+was still one. `terminal_reader` now removes the session before publishing
+Exit/Error. The regression observes the count synchronously in the event
+callback, so it no longer relies on the receiving thread winning a race.
+That deterministic test fails against the original implementation and passes
+after the ordering fix. The Windows reset, cancellation and provider regression
+subsets passed on `6e97c73`, including the dead-leader helper test. macOS tests
+and strict Clippy also passed on that head. The terminal ordering fix triggers
+another CI run; packaged-app performance certification remains separate.
diff --git a/ui/src/views/ResetDialog.tsx b/ui/src/views/ResetDialog.tsx
index da82dc9..9734236 100644
--- a/ui/src/views/ResetDialog.tsx
+++ b/ui/src/views/ResetDialog.tsx
@@ -105,7 +105,7 @@ export function ResetDialog({
{option('soft', 'Soft', 'keep all changes staged')}
{option('mixed', 'Mixed', 'keep changes, unstaged')}
- {option('hard', 'Hard', 'discard all changes (a safety snapshot stash is saved first)', true)}
+ {option('hard', 'Hard', 'discard tracked changes after a safety snapshot; refuse untracked or ignored file collisions', true)}
{error ? {error}
: null}
diff --git a/website/docs/commits-and-history.md b/website/docs/commits-and-history.md
index 6e223e5..1147803 100644
--- a/website/docs/commits-and-history.md
+++ b/website/docs/commits-and-history.md
@@ -110,6 +110,12 @@ Unlike the graph, the reflog includes commits orphaned by a reset, rebase, or am
To recover a commit you lost to a bad reset: open the Reflog, find the entry from before the reset, and either **Create branch here…** to keep it or **Reset HEAD here…** to move your branch back. If the commit is orphaned it won't appear in the graph, but the context menu actions work on it directly.
+**Hard reset** snapshots tracked changes before discarding them. It refuses to
+overwrite untracked or ignored files that collide with the target commit,
+including file/folder replacements. Move or commit those files before retrying.
+Unrelated untracked files remain in place; a clean reset does not create an
+empty recovery stash.
+
## Work file documents
Open any file from the sidebar's **Files** tab or the command palette to get a
diff --git a/website/docs/everyday-git.md b/website/docs/everyday-git.md
index b3ff4e3..6b1bd3f 100644
--- a/website/docs/everyday-git.md
+++ b/website/docs/everyday-git.md
@@ -16,6 +16,9 @@ Right-click a single file in Local Changes or Review and choose **Open in editor
- The view opens with a "show all" stacked diff of every changed file. Clicking the Unstaged or Staged column title re-selects that side's full changeset, and selecting a folder row aggregates the diffs beneath it.
- Stage or unstage a whole file from its row, or use **Stage all** / **Unstage all** for the whole side.
+ Filenames such as `[id].tsx` are treated literally; selecting one file does
+ not select other matching names. A symlink can be staged even when its target
+ does not exist.
- Multi-select files and folders with `Mod`-click or Shift-click. Stage, Unstage, Stash, and Discard act on every selected file plus every changed file beneath each selected folder.
- **Block and line staging**: each change block in the diff has inline **Stage** and **Discard** buttons (**Unstage** on the staged side). Drag across changed line numbers to act on a contiguous range, or choose **Lines…** for a keyboard-operable checklist that can select any combination of deleted and added lines. The action labels show the selected-line count.
- **Discarding a change block or selected lines is recoverable**: it shows an Undo toast for a few seconds. Whole-file and bulk discards are immediate and permanent — there is no Undo toast and no automatic safety stash — so stash first if you might want the changes back.
@@ -53,12 +56,16 @@ Open a row's context menu with right-click, the Menu key, or `Shift+F10`:
folder row.
- A file can jump directly to **Open file history** or **Open blame**.
- **New file here…**, **New folder here…**, and **Rename / move…** act relative
- to the selected row.
+ to the selected row. Rename/move refuses paths inside Git metadata, including
+ aliases into `.git`.
- Copy one or several relative paths, or native absolute paths suitable for
the current operating system.
- **Delete file/folder** requires a second confirmation click. Tracked entries
become ordinary working-tree deletions; the index is not changed.
+**Ignore** refuses to edit a `.gitignore` that is a symlink, so the action
+cannot change the file that link points to.
+
The Files tree uses the repository's ignored-inclusive local listing directly;
it does not first substitute the Git snapshot while that listing loads. Current
Git state is overlaid on those local paths, so added, modified, and deleted
@@ -225,7 +232,8 @@ Use **Previous page** / **Next page** for larger lists and **Open repository**
to work in a module's own tab.
Progress and errors remain visible. **Cancel operation** stops Git and its
-helpers. Completed clones and local objects remain available: refresh, inspect
+helpers, including helpers left running after the parent exits on Windows.
+Completed clones and local objects remain available: refresh, inspect
the current state, correct the error and retry. Git's transport restrictions
still apply, including restrictions on local-file submodule URLs.
diff --git a/website/docs/work.md b/website/docs/work.md
index 744fd17..5751334 100644
--- a/website/docs/work.md
+++ b/website/docs/work.md
@@ -59,6 +59,8 @@ buffer and reload the file from disk without writing it. Historical revisions,
binaries, oversized files, and non-UTF-8 text
stay read-only. If another tool changes the file while you have unsaved edits,
Strand refuses the stale save instead of overwriting the newer disk content.
+Large working-tree text files show a read-only preview of their first 2 MB;
+Strand reads only that prefix rather than loading the whole file.
If a file moves through Strand, its tabs follow the new path. A removed preview
closes; a removed pinned file stays visible with a clear missing-file message.