mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
feat(sessions): prune pasted images no session still names
Refs #7221 (CW-7804). Pasted clipboard images are written as flat clipboard-<nanos>.png files under ~/.codewhale/clipboard-images/ and nothing ever removed them; the reporter's Windows install grew by several hundred MB in a month of QQ screenshots. `codewhale sessions prune-images` sits beside `scrub-secrets` and keeps its shape: report by default, `--apply` to delete. An image is offered only when no file under the Codewhale state directory, the sessions directory or the Runtime task store names it (saved sessions, checkpoints, archives, Runtime threads, the composer stash and history) and it is older than the grace window (`--older-than-days`, default 7), so an unsent draft or a running turn keeps its picture. The scan reads raw bytes and needs no knowledge of each store's schema; it never follows symlinks inside the roots and stops once every image is accounted for. If any file or directory cannot be read, nothing is deleted. A file another process holds open (common on Windows) is reported and skipped. A missing image never fails resume: the bytes enter history when a message is built (expand_attachment_blocks), and re-sending an old message reports the missing file in-band. The traversal reuses session_secret_scrub's no-follow directory helpers (now pub(crate)). docs/READ_MEDIA.md documents the command and a features.toml row registers it. Known limits: references held only outside the scanned roots (for example a workspace-local .codewhale directory) are not seen; age-only pruning stays out of scope; grouping new pastes by session is not done. Verified (macOS, offline, locked): the batch's six commits were tested as one change on Boat farm VMs (Linux x86_64), shipping only `git diff <base> <final>`, so no other session's uncommitted files took part. cargo test --locked --lib with filters over config, runtime, engine, app-server, tui and cli on base3631a174f6: 459 passed, 0 failed (config 8, runtime 117, engine 102, tui 220, cli 8, app-server 4; farm run 20261107T230833Z-issues-tests-503e01). cargo clippy with the Lint job's flags (--all-targets --all-features, -D warnings) on config, runtime, engine, command-contract, cli, tui and app-server: 0 warnings, 0 errors (run 20261107T231817Z-issues-clippy-e84152). Rebased onto5ba1aa63bc(eager sub-agents), which shares subagent/mod.rs, subagent/tests.rs, tasks.rs and config.example.toml with this batch; this batch's changed lines are byte-identical on both bases, and a recheck of that overlap on the new base passed: engine 48 and app-server 2 passed, 0 failed (subagent coordination incl. the two-process case, human-wait, execution-guard and tools::tasks suites, server_task_config and server_task_runtime; farm run 20261108T000442Z-issues-recheck-67ab1a). Locally: cargo fmt --all -- --check, both budget checks, sync-changelog --check and the feature release-note receipts pass. Covers the session_secret_scrub suite whose no-follow directory helpers this reuses, and the build of the new subcommand. The fixture run of `codewhale sessions prune-images` on a macOS binary follows from the freeze build. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
711196c671
commit
c98ea3836e
9 files changed
+338
-4
No files matched your search
@@ -70,6 +70,12 @@ until the matching tag, packages, checksums, and release assets exist.
|
||||
|
||||
### Added
|
||||
|
||||
- **Prune pasted images:** `codewhale sessions prune-images` lists pasted
|
||||
clipboard images that no saved session, checkpoint, Runtime thread, draft or
|
||||
history still names and that are older than 7 days (`--older-than-days`);
|
||||
`--apply` deletes them. If any file it must scan cannot be read, it deletes
|
||||
nothing. Run it while no Codewhale session is open. Thanks
|
||||
[@Asteria1028](https://github.com/Asteria1028) for the report (Refs #7221).
|
||||
- **Task wall clock pauses for answerable human waits (#5898):** a durable task run by the Runtime API server no longer spends its wall-time budget while a prompt on that run waits for an approval or user-input answer. `[tools] task_human_wait_cap_seconds` (default 1800, maximum 86400) bounds the total wait and fails an unanswered task with `human_wait_timeout`, releasing its worker. Only a Runtime serving its HTTP API pauses the clock; local Stdio, Socket and ACP control frontends keep the wall deadline, and a wall deadline crossed before the prompt still ends the task. The TUI's own tasks are unchanged. Thanks [@asto18089](https://github.com/asto18089).
|
||||
- **MCP servers say why they are not connected:** the Runtime API's
|
||||
`GET /v1/apps/mcp/servers` now includes a configured server's last
|
||||
@@ -613,6 +619,7 @@ until the matching tag, packages, checksums, and release assets exist.
|
||||
|
||||
### Contributors
|
||||
|
||||
- **[@Asteria1028](https://github.com/Asteria1028)** — reported pasted clipboard images piling up with no way to remove them ([#7221](https://github.com/codewhale-hq/Codewhale/issues/7221)).
|
||||
- **[@zhuowp](https://github.com/zhuowp)** (git author `z27014`) — added bounded HTTP rate-limit recovery and corrected retry deadlines, quota guidance and recovery receipts ([#7220](https://github.com/codewhale-hq/Codewhale/pull/7220)).
|
||||
- **[@printf033](https://github.com/printf033)** — enabled exclusive rename on Android so Fleet publication does not fall back to SELinux-denied hard links (`fbe9f24ee`).
|
||||
- **[@liuyy2](https://github.com/liuyy2)** — diagnosed case-sensitive model selection collisions in the provider picker ([#7214](https://github.com/codewhale-hq/Codewhale/issues/7214)).
|
||||
|
||||
@@ -59,6 +59,7 @@ pub mod runtime_options;
|
||||
pub mod runtime_threads;
|
||||
pub mod scorecard;
|
||||
pub mod session_export;
|
||||
pub mod session_image_prune;
|
||||
pub mod session_manager;
|
||||
pub mod session_peek;
|
||||
pub mod session_projection;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::io::{self, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::session_secret_scrub::{directory_entries, entry_has_type};
|
||||
|
||||
pub const PRUNE_IMAGES_COMMAND: &str = "codewhale sessions prune-images";
|
||||
|
||||
const NAME_PREFIX: &[u8] = b"clipboard-";
|
||||
const NAME_SUFFIX: &[u8] = b".png";
|
||||
const SCAN_CHUNK: usize = 1 << 20;
|
||||
const NAME_CARRY: usize = 96;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ImagePruneReport {
|
||||
pub images: usize,
|
||||
pub image_bytes: u64,
|
||||
pub referenced: usize,
|
||||
pub recent: usize,
|
||||
pub unreferenced: Vec<(PathBuf, u64)>,
|
||||
pub removed: Vec<(PathBuf, u64)>,
|
||||
pub failed: Vec<(PathBuf, String)>,
|
||||
pub files_scanned: usize,
|
||||
pub unreadable: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl ImagePruneReport {
|
||||
#[must_use]
|
||||
pub fn unreferenced_bytes(&self) -> u64 {
|
||||
self.unreferenced.iter().map(|(_, bytes)| bytes).sum()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn removed_bytes(&self) -> u64 {
|
||||
self.removed.iter().map(|(_, bytes)| bytes).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_clipboard_image_name(name: &str) -> bool {
|
||||
name.strip_prefix("clipboard-")
|
||||
.and_then(|rest| rest.strip_suffix(".png"))
|
||||
.is_some_and(|digits| !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()))
|
||||
}
|
||||
|
||||
pub fn prune_clipboard_images(
|
||||
image_dir: &Path,
|
||||
scan_roots: &[PathBuf],
|
||||
grace: Duration,
|
||||
apply: bool,
|
||||
) -> ImagePruneReport {
|
||||
let mut report = ImagePruneReport::default();
|
||||
let mut images: BTreeMap<String, (PathBuf, u64, SystemTime)> = BTreeMap::new();
|
||||
for entry in directory_entries(image_dir, &mut report.unreadable) {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if !is_clipboard_image_name(&name) || !entry_has_type(&entry, false, &mut report.unreadable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
match entry.metadata() {
|
||||
Ok(meta) => {
|
||||
report.image_bytes += meta.len();
|
||||
images.insert(
|
||||
name,
|
||||
(
|
||||
entry.path(),
|
||||
meta.len(),
|
||||
meta.modified().unwrap_or(SystemTime::now()),
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(_) => report.unreadable.push(entry.path()),
|
||||
}
|
||||
}
|
||||
report.images = images.len();
|
||||
if images.is_empty() {
|
||||
return report;
|
||||
}
|
||||
|
||||
let excluded = std::fs::canonicalize(image_dir).unwrap_or_else(|_| image_dir.to_path_buf());
|
||||
let mut referenced = HashSet::new();
|
||||
let mut seen_roots = HashSet::new();
|
||||
let mut pending: Vec<PathBuf> = Vec::new();
|
||||
for root in scan_roots {
|
||||
if let Ok(root) = std::fs::canonicalize(root)
|
||||
&& seen_roots.insert(root.clone())
|
||||
{
|
||||
pending.push(root);
|
||||
}
|
||||
}
|
||||
let mut visited_dirs = HashSet::new();
|
||||
while let Some(dir) = pending.pop() {
|
||||
if dir == excluded || !visited_dirs.insert(dir.clone()) {
|
||||
continue;
|
||||
}
|
||||
for entry in directory_entries(&dir, &mut report.unreadable) {
|
||||
let Ok(kind) = entry.file_type() else {
|
||||
report.unreadable.push(entry.path());
|
||||
continue;
|
||||
};
|
||||
if kind.is_dir() {
|
||||
pending.push(entry.path());
|
||||
} else if kind.is_file() {
|
||||
report.files_scanned += 1;
|
||||
if scan_file(&entry.path(), &mut referenced).is_err() {
|
||||
report.unreadable.push(entry.path());
|
||||
}
|
||||
if images
|
||||
.keys()
|
||||
.all(|name| referenced.contains(name.as_bytes()))
|
||||
{
|
||||
pending.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let now = SystemTime::now();
|
||||
for (name, (path, bytes, modified)) in images {
|
||||
if referenced.contains(name.as_bytes()) {
|
||||
report.referenced += 1;
|
||||
} else if now.duration_since(modified).unwrap_or_default() < grace {
|
||||
report.recent += 1;
|
||||
} else {
|
||||
report.unreferenced.push((path, bytes));
|
||||
}
|
||||
}
|
||||
report.unreadable.sort();
|
||||
report.unreadable.dedup();
|
||||
if apply && report.unreadable.is_empty() {
|
||||
for (path, bytes) in std::mem::take(&mut report.unreferenced) {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => report.removed.push((path, bytes)),
|
||||
Err(error) => report.failed.push((path, error.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
report
|
||||
}
|
||||
|
||||
fn scan_file(path: &Path, referenced: &mut HashSet<Vec<u8>>) -> io::Result<()> {
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
let mut buffer = vec![0_u8; SCAN_CHUNK + NAME_CARRY];
|
||||
let mut carried = 0;
|
||||
loop {
|
||||
let read = file.read(&mut buffer[carried..])?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
let filled = carried + read;
|
||||
let window = &buffer[..filled];
|
||||
let mut start = 0;
|
||||
while let Some(offset) = find(&window[start..], NAME_PREFIX) {
|
||||
let begin = start + offset;
|
||||
let digits_start = begin + NAME_PREFIX.len();
|
||||
let digits_end = window[digits_start..]
|
||||
.iter()
|
||||
.position(|byte| !byte.is_ascii_digit())
|
||||
.map_or(filled, |len| digits_start + len);
|
||||
let end = digits_end + NAME_SUFFIX.len();
|
||||
if digits_end > digits_start && end <= filled && &window[digits_end..end] == NAME_SUFFIX
|
||||
{
|
||||
referenced.insert(window[begin..end].to_vec());
|
||||
}
|
||||
start = digits_start;
|
||||
}
|
||||
carried = filled.min(NAME_CARRY);
|
||||
buffer.copy_within(filled - carried..filled, 0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||
haystack
|
||||
.windows(needle.len())
|
||||
.position(|window| window == needle)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ fn canonical_store_root(dir: &Path, unreadable: &mut Vec<PathBuf>) -> Option<Pat
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_has_type(
|
||||
pub(crate) fn entry_has_type(
|
||||
entry: &std::fs::DirEntry,
|
||||
directory: bool,
|
||||
unreadable: &mut Vec<PathBuf>,
|
||||
@@ -159,7 +159,10 @@ fn entry_has_type(
|
||||
}
|
||||
}
|
||||
|
||||
fn directory_entries(dir: &Path, unreadable: &mut Vec<PathBuf>) -> Vec<std::fs::DirEntry> {
|
||||
pub(crate) fn directory_entries(
|
||||
dir: &Path,
|
||||
unreadable: &mut Vec<PathBuf>,
|
||||
) -> Vec<std::fs::DirEntry> {
|
||||
let entries = match std::fs::symlink_metadata(dir) {
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Vec::new(),
|
||||
Ok(meta) if meta.is_dir() => std::fs::read_dir(dir).ok(),
|
||||
|
||||
@@ -70,6 +70,12 @@ until the matching tag, packages, checksums, and release assets exist.
|
||||
|
||||
### Added
|
||||
|
||||
- **Prune pasted images:** `codewhale sessions prune-images` lists pasted
|
||||
clipboard images that no saved session, checkpoint, Runtime thread, draft or
|
||||
history still names and that are older than 7 days (`--older-than-days`);
|
||||
`--apply` deletes them. If any file it must scan cannot be read, it deletes
|
||||
nothing. Run it while no Codewhale session is open. Thanks
|
||||
[@Asteria1028](https://github.com/Asteria1028) for the report (Refs #7221).
|
||||
- **Task wall clock pauses for answerable human waits (#5898):** a durable task run by the Runtime API server no longer spends its wall-time budget while a prompt on that run waits for an approval or user-input answer. `[tools] task_human_wait_cap_seconds` (default 1800, maximum 86400) bounds the total wait and fails an unanswered task with `human_wait_timeout`, releasing its worker. Only a Runtime serving its HTTP API pauses the clock; local Stdio, Socket and ACP control frontends keep the wall deadline, and a wall deadline crossed before the prompt still ends the task. The TUI's own tasks are unchanged. Thanks [@asto18089](https://github.com/asto18089).
|
||||
- **MCP servers say why they are not connected:** the Runtime API's
|
||||
`GET /v1/apps/mcp/servers` now includes a configured server's last
|
||||
@@ -613,6 +619,7 @@ until the matching tag, packages, checksums, and release assets exist.
|
||||
|
||||
### Contributors
|
||||
|
||||
- **[@Asteria1028](https://github.com/Asteria1028)** — reported pasted clipboard images piling up with no way to remove them ([#7221](https://github.com/codewhale-hq/Codewhale/issues/7221)).
|
||||
- **[@zhuowp](https://github.com/zhuowp)** (git author `z27014`) — added bounded HTTP rate-limit recovery and corrected retry deadlines, quota guidance and recovery receipts ([#7220](https://github.com/codewhale-hq/Codewhale/pull/7220)).
|
||||
- **[@printf033](https://github.com/printf033)** — enabled exclusive rename on Android so Fleet publication does not fall back to SELinux-denied hard links (`fbe9f24ee`).
|
||||
- **[@liuyy2](https://github.com/liuyy2)** — diagnosed case-sensitive model selection collisions in the provider picker ([#7214](https://github.com/codewhale-hq/Codewhale/issues/7214)).
|
||||
|
||||
+100
-2
@@ -8,8 +8,8 @@
|
||||
|
||||
use codewhale_engine::runtime_options::RuntimeOptions;
|
||||
use codewhale_engine::{
|
||||
fleet, pricing, project_context, receipts, runtime_threads, session_export, session_manager,
|
||||
session_secret_scrub, task_manager,
|
||||
fleet, pricing, project_context, receipts, runtime_threads, session_export,
|
||||
session_image_prune, session_manager, session_secret_scrub, task_manager,
|
||||
};
|
||||
use codewhale_runtime::config;
|
||||
use std::collections::BTreeSet;
|
||||
@@ -308,6 +308,17 @@ enum SessionsCommand {
|
||||
#[arg(long, default_value_t = false)]
|
||||
apply: bool,
|
||||
},
|
||||
/// Delete pasted clipboard images that no saved session, draft or history
|
||||
/// still names. Reports what it would delete unless `--apply` is given.
|
||||
/// Run it while no Codewhale session is open.
|
||||
PruneImages {
|
||||
/// Delete the listed images (default: report only)
|
||||
#[arg(long, default_value_t = false)]
|
||||
apply: bool,
|
||||
/// Keep unreferenced images newer than this many days
|
||||
#[arg(long, value_name = "DAYS", default_value_t = 7)]
|
||||
older_than_days: u64,
|
||||
},
|
||||
/// Export a session as a full-fidelity tar.xz archive (complete context:
|
||||
/// system prompt, messages, tool calls and results, plus artifacts)
|
||||
Export {
|
||||
@@ -2626,6 +2637,10 @@ async fn run_async_main_dispatch(
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(SessionsCommand::PruneImages {
|
||||
apply,
|
||||
older_than_days,
|
||||
}) => run_sessions_prune_images(apply, older_than_days).await,
|
||||
Some(SessionsCommand::Export {
|
||||
id,
|
||||
output,
|
||||
@@ -9771,6 +9786,89 @@ fn run_sessions_scrub_secrets_blocking(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_sessions_prune_images(apply: bool, older_than_days: u64) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
let ticket = crate::test_support::env_scope_ticket();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
#[cfg(test)]
|
||||
let _membership = crate::test_support::join_env_scope(ticket);
|
||||
run_sessions_prune_images_blocking(apply, older_than_days)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
fn run_sessions_prune_images_blocking(apply: bool, older_than_days: u64) -> Result<()> {
|
||||
let workspace = std::env::current_dir().context("Failed to read the current directory")?;
|
||||
let image_dir = config::clipboard_images_dir(&workspace);
|
||||
let manager = session_manager::SessionManager::default_location()?;
|
||||
let runtime = runtime_threads::manager::RuntimeThreadManagerConfig::from_task_data_dir(
|
||||
task_manager::default_tasks_dir(),
|
||||
);
|
||||
let mut roots = vec![
|
||||
manager.sessions_dir().to_path_buf(),
|
||||
runtime.data_dir.clone(),
|
||||
];
|
||||
roots.extend(image_dir.parent().map(Path::to_path_buf));
|
||||
let grace = Duration::from_secs(older_than_days.saturating_mul(24 * 60 * 60));
|
||||
let report = session_image_prune::prune_clipboard_images(&image_dir, &roots, grace, apply);
|
||||
if report.images == 0 {
|
||||
println!("No pasted images in {}.", image_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
println!(
|
||||
"{} pasted images ({}) in {}: {} still referenced, {} newer than {older_than_days} days.",
|
||||
report.images,
|
||||
format_bytes(report.image_bytes),
|
||||
image_dir.display(),
|
||||
report.referenced,
|
||||
report.recent,
|
||||
);
|
||||
if !report.removed.is_empty() {
|
||||
println!(
|
||||
"Deleted {} unreferenced images ({}).",
|
||||
report.removed.len(),
|
||||
format_bytes(report.removed_bytes())
|
||||
);
|
||||
}
|
||||
if !report.unreferenced.is_empty() {
|
||||
println!(
|
||||
"{} unreferenced images ({}) {}:",
|
||||
report.unreferenced.len(),
|
||||
format_bytes(report.unreferenced_bytes()),
|
||||
if apply { "were kept" } else { "can be deleted" }
|
||||
);
|
||||
for (path, _) in &report.unreferenced {
|
||||
println!(" {}", path.display());
|
||||
}
|
||||
if !apply && report.unreadable.is_empty() {
|
||||
println!(
|
||||
"Re-run with `{} --apply` to delete them (close open Codewhale sessions first).",
|
||||
session_image_prune::PRUNE_IMAGES_COMMAND
|
||||
);
|
||||
}
|
||||
}
|
||||
if !report.failed.is_empty() {
|
||||
println!(
|
||||
"{} images could not be deleted and were left in place:",
|
||||
report.failed.len()
|
||||
);
|
||||
for (path, error) in &report.failed {
|
||||
println!(" {}: {error}", path.display());
|
||||
}
|
||||
}
|
||||
if !report.unreadable.is_empty() {
|
||||
println!(
|
||||
"{} files or directories could not be read, so `--apply` deletes nothing until they can:",
|
||||
report.unreadable.len()
|
||||
);
|
||||
for path in &report.unreadable {
|
||||
println!(" {}", path.display());
|
||||
}
|
||||
}
|
||||
println!("Scanned {} files for references.", report.files_scanned);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_sessions(limit: usize, search: Option<String>) -> Result<()> {
|
||||
use codewhale_palette as palette;
|
||||
use colored::Colorize;
|
||||
|
||||
@@ -35,6 +35,32 @@
|
||||
that explicit turn action; merely having a screenshot path or artifact does
|
||||
not trigger background analysis.
|
||||
|
||||
### Pasted image storage
|
||||
|
||||
A pasted clipboard image is saved as `clipboard-<number>.png` under
|
||||
`~/.codewhale/clipboard-images/` (or `<workspace>/clipboard-images/` when no
|
||||
home directory is available), and the attachment names that absolute path.
|
||||
The image bytes enter the request when the message is built, so a later
|
||||
missing file never fails a resume; editing and resending an old message
|
||||
reattaches from the path and reports a missing image instead of sending it.
|
||||
|
||||
Nothing deletes these files on its own. To reclaim space:
|
||||
|
||||
```sh
|
||||
codewhale sessions prune-images # report only
|
||||
codewhale sessions prune-images --apply # delete what the report lists
|
||||
codewhale sessions prune-images --older-than-days 30
|
||||
```
|
||||
|
||||
The command lists only images that no file under the Codewhale state
|
||||
directory, the sessions directory or the Runtime task store still names
|
||||
(saved sessions, checkpoints, archives, Runtime threads, the composer stash
|
||||
and history) and that are older than the grace window (7 days by default),
|
||||
so a draft or a running turn keeps its picture. If any file there cannot be
|
||||
read, it reports what it found and deletes nothing. A file another process
|
||||
still holds open (common on Windows) is reported and skipped. Close open
|
||||
Codewhale sessions before `--apply`.
|
||||
|
||||
### How `Ctrl+V` reaches Codewhale
|
||||
|
||||
Most terminals own the paste chord, and Windows Terminal turns `Ctrl+V` into a
|
||||
|
||||
@@ -509,6 +509,16 @@ surfaces = { tui = "stable" }
|
||||
docs = "docs/READ_MEDIA.md"
|
||||
owner = "crates/tui/src/tui/image_preview.rs"
|
||||
|
||||
[[feature]]
|
||||
id = "clipboard-image-prune"
|
||||
name = "Pasted image cleanup"
|
||||
summary = "codewhale sessions prune-images reports, and with --apply deletes, pasted clipboard images that no saved session, draft or history still names."
|
||||
status = "experimental"
|
||||
surfaces = { tui = "preview" }
|
||||
since = "unreleased"
|
||||
docs = "docs/READ_MEDIA.md#pasted-image-storage"
|
||||
owner = "crates/engine/src/session_image_prune.rs"
|
||||
|
||||
[[feature]]
|
||||
id = "memory"
|
||||
name = "User memory"
|
||||
|
||||
@@ -189,6 +189,9 @@
|
||||
"crates/engine/src/session_export.rs": {
|
||||
"path_canonicalize": 1
|
||||
},
|
||||
"crates/engine/src/session_image_prune.rs": {
|
||||
"std_fs": 4
|
||||
},
|
||||
"crates/engine/src/session_manager.rs": {
|
||||
"path_canonicalize": 2,
|
||||
"thread_sleep": 1
|
||||
|
||||
Reference in new issue
Block a user