diff --git a/Cargo.lock b/Cargo.lock index 53a5bfe..9ee76d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,7 +251,7 @@ dependencies = [ [[package]] name = "tcap" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 851fb3a..5804279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tcap" -version = "0.1.1" +version = "0.1.2" edition = "2021" description = "Capture the last command and its output from your terminal" license = "MIT" diff --git a/README.md b/README.md index 6c56e2e..ef41bc1 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,8 @@ wrong block. **On kitty, output is the last *non-empty* output.** kitty counts the running `tcap` as the current command, so it has to be asked for the last output that wasn't empty. The consequence: if your last command printed nothing, kitty -returns an *older* command's output while the header names the last one. The -annotated header carries a `# note:` line saying so, and `--json` sets -`"approximate": true`. Two `tcap` runs in a row are a **hard error** rather than +returns an *older* command's output while the header names the last one. The annotated header carries a `# note:` line saying so and `--json` sets +`"approximate": true`; a one-line reminder goes to stderr once per session. Two `tcap` runs in a row are a **hard error** rather than a label, since there the mismatch is certain. tmux has no such ambiguity, because the shell hook records real boundaries. kitty also needs remote control reachable — either `allow_remote_control yes`, or diff --git a/npm/README.md b/npm/README.md index 853364c..8d7f0a1 100644 --- a/npm/README.md +++ b/npm/README.md @@ -112,8 +112,9 @@ block. **On kitty, output is the last *non-empty* output**, because kitty counts the running `tcap` as the current command. A command that printed nothing therefore -yields an older output under the current header; it is labelled with a `# note:` -line and `"approximate": true` in `--json`. Two `tcap` runs in a row are a hard +yields an older output under the current header; it is labelled with a `# note:` line and +`"approximate": true` in `--json`, plus a one-line stderr reminder once per +session. Two `tcap` runs in a row are a hard error instead, since the mismatch is certain there. tmux has no such ambiguity. **tmux wins whenever it's running.** Inside kitty running tmux, asking kitty for diff --git a/npm/package.json b/npm/package.json index 720365e..caa9e84 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@agora-build/tcap", - "version": "0.1.1", + "version": "0.1.2", "description": "Capture the last command and its output from your terminal, ready to pipe into an LLM", "bin": { "tcap": "bin/tcap" diff --git a/src/main.rs b/src/main.rs index 4d895fa..cacfc76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -246,12 +246,16 @@ fn emit_hints(caps: &[Capture], backend: &dyn backend::Backend, mode: cli::Mode) // // The *certain* mismatch — a capture straight after another one — does not // reach here at all; capture_and_print refuses it outright. - if mode != cli::Mode::Command && caps.iter().any(|c| c.approximate) { + // Once per session, and one line. This is true of *every* kitty capture, so + // repeating it on each one is noise that buries the hints that are specific + // — and `tcap | llm` is the workflow it interrupted most. + if mode != cli::Mode::Command + && caps.iter().any(|c| c.approximate) + && state::first_time("approximate") + { eprintln!( - "tcap: {} returns the last non-empty output, which may not belong to the\n\ - \x20 command named above — a silent command yields an earlier one's output,\n\ - \x20 and a previous `tcap | …` leaves its pipeline's. tmux records exact\n\ - \x20 boundaries; --quiet silences this.", + "tcap: {} cannot tie output to a command, so this may be an earlier one's \ + — see `tcap doctor`. Shown once per session; --quiet silences it.", backend.name() ); printed = true; diff --git a/src/state.rs b/src/state.rs index 4e72caa..4fde039 100644 --- a/src/state.rs +++ b/src/state.rs @@ -63,6 +63,20 @@ pub fn take_capture_marker() -> bool { fs::remove_file(marker_path()).is_ok() } +/// True the first time it is asked for `key` in this session. +/// +/// A caveat that is always true — kitty can never attribute output to a command +/// — says nothing when repeated on every capture, and trains the reader to skip +/// it. Said once, it still lands. +pub fn first_time(key: &str) -> bool { + let path = state_dir().join(format!("{}.said-{key}", session_key())); + if fs::symlink_metadata(&path).is_ok() { + return false; + } + // If the note cannot be recorded, saying it again is the safer failure. + secure_write(&path, b"1").is_ok() +} + /// Why the state directory is unusable, if it is. /// /// `load` failing closed to an empty history also switches off the guard that