mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
feat(daemon): keep every pane's screen, without asking
`persist_scrollback` is gone, and with it the switch, its three translations and the branches that read it. Keeping a capped tail of each pane's output is now what the daemon does, not something it can be asked to do. This reverses the call made when the feature landed. The argument for off-by-default was that the ring holds whatever the pane printed — echoed tokens, `env` output, an agent's transcript — and that writing that down should be the user's decision to make. What the argument missed is when the decision gets made: the moment anyone learns they wanted this is the moment a daemon has already died, and by then the setting could only be turned on for next time. A feature whose entire purpose is to survive an event nobody schedules cannot be opt-in. The cost is real and does not go away: pane output now lives at `<config>/scrollback/*.bin` on every machine, 0600 on unix and behind the config directory's ACL on Windows, capped at 256 KiB per pane and dropped as soon as no window can still ask for it. Old configs naming the key still parse — nothing in `Config` refuses unknown fields — so the key simply stops meaning anything.
This commit is contained in:
@@ -277,17 +277,6 @@ pub struct Config {
|
||||
pub agent_commands: HashMap<String, String>,
|
||||
#[serde(default = "default_true")]
|
||||
pub restore_agent_sessions: bool,
|
||||
/// Keep a capped tail of each pane's output on disk, so a daemon that dies
|
||||
/// without getting to hand off — a crash, a `kill -9`, a reboot — comes
|
||||
/// back to panes that still show what was in them.
|
||||
///
|
||||
/// Off by default, and the default is the interesting part. What the ring
|
||||
/// holds is whatever the pane printed, which routinely includes secrets:
|
||||
/// an echoed token, the output of `env`, an agent's transcript. In memory
|
||||
/// they die with the daemon. Writing them down is the entire feature and
|
||||
/// also its entire cost, so it is the user who decides to pay it.
|
||||
#[serde(default)]
|
||||
pub persist_scrollback: bool,
|
||||
/// Give each pane its own shell history instead of one file every pane
|
||||
/// appends to and reads back.
|
||||
///
|
||||
@@ -556,7 +545,6 @@ impl Default for Config {
|
||||
command_frecency: HashMap::new(),
|
||||
agent_commands: HashMap::new(),
|
||||
restore_agent_sessions: true,
|
||||
persist_scrollback: false,
|
||||
per_pane_history: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,11 +143,6 @@ fn take<'a>(cur: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
|
||||
Some(head)
|
||||
}
|
||||
|
||||
/// Whether the user has asked for scrollback to outlive the daemon.
|
||||
pub fn enabled() -> bool {
|
||||
crate::core::config::Config::load().persist_scrollback
|
||||
}
|
||||
|
||||
fn dir() -> Option<PathBuf> {
|
||||
crate::core::config::config_path("scrollback")
|
||||
}
|
||||
|
||||
@@ -182,28 +182,14 @@ fn restorable_pane_ids(registry: &Registry) -> std::collections::HashSet<u64> {
|
||||
///
|
||||
/// Only panes whose ring has moved are written, so an idle machine does no IO
|
||||
/// at all, and the busy pane that most needs a fresh copy is the one that gets
|
||||
/// it. Turning the setting off mid-run is honoured here too: the next tick
|
||||
/// clears the directory rather than leaving terminal output on disk that the
|
||||
/// user has just said they do not want stored.
|
||||
/// it.
|
||||
fn spawn_scrollback_writer(registry: Arc<Registry>) {
|
||||
let spawned = std::thread::Builder::new()
|
||||
.name("tty7-scrollback".into())
|
||||
.spawn(move || {
|
||||
let mut marks: HashMap<u64, u64> = HashMap::new();
|
||||
let mut storing = crate::daemon::scrollback::enabled();
|
||||
loop {
|
||||
std::thread::sleep(crate::daemon::scrollback::SNAPSHOT_INTERVAL);
|
||||
let enabled = crate::daemon::scrollback::enabled();
|
||||
if !enabled {
|
||||
if storing {
|
||||
log::info!("scrollback persistence turned off; dropping what was stored");
|
||||
crate::daemon::scrollback::sweep(&std::collections::HashSet::new());
|
||||
marks.clear();
|
||||
storing = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
storing = true;
|
||||
for pane in registry.all() {
|
||||
if marks.get(&pane.id) == Some(&pane.scrollback_mark()) {
|
||||
continue;
|
||||
@@ -228,9 +214,6 @@ fn spawn_scrollback_writer(registry: Arc<Registry>) {
|
||||
/// covers the ones we do, and makes the copy exact rather than up to
|
||||
/// [`SNAPSHOT_INTERVAL`](crate::daemon::scrollback::SNAPSHOT_INTERVAL) stale.
|
||||
fn store_scrollback_now(registry: &Registry) {
|
||||
if !crate::daemon::scrollback::enabled() {
|
||||
return;
|
||||
}
|
||||
for pane in registry.all() {
|
||||
let (segments, _) = pane.scrollback_snapshot();
|
||||
crate::daemon::scrollback::save(pane.id, &segments);
|
||||
@@ -246,9 +229,6 @@ fn store_scrollback_now(registry: &Registry) {
|
||||
fn restored_screen(
|
||||
request: crate::daemon::protocol::RestoreFrom,
|
||||
) -> Option<crate::daemon::pane::Restore> {
|
||||
if !crate::daemon::scrollback::enabled() {
|
||||
return None;
|
||||
}
|
||||
let segments = crate::daemon::scrollback::load(request.pane_id)?;
|
||||
if segments.is_empty() {
|
||||
return None;
|
||||
@@ -570,25 +550,18 @@ fn run_with(registry: Arc<Registry>) -> anyhow::Result<()> {
|
||||
|
||||
spawn_orphan_sweep(registry.clone());
|
||||
let restorable = restorable_pane_ids(®istry);
|
||||
// Deliberately not swept here while the setting is on. Startup is the one
|
||||
// moment this process knows least: it owns no panes yet, and the windows
|
||||
// that know which screens are still wanted cannot say so until the
|
||||
// endpoint below is listening. Answering "is anyone going to ask for
|
||||
// this?" here answers it when nobody can — and the answer deletes. A tree
|
||||
// that failed to parse makes it worse, because `read_machine` quarantines
|
||||
// it and hands back an empty `Machine`, so one bad file would take every
|
||||
// pane's screen with it.
|
||||
// No scrollback sweep here, deliberately. Startup is the one moment this
|
||||
// process knows least: it owns no panes yet, and the windows that know
|
||||
// which screens are still wanted cannot say so until the endpoint below is
|
||||
// listening. Answering "is anyone going to ask for this?" here answers it
|
||||
// when nobody can — and the answer deletes. A tree that failed to parse
|
||||
// makes it worse, because `read_machine` quarantines it and hands back an
|
||||
// empty `Machine`, so one bad file would take every pane's screen with it.
|
||||
//
|
||||
// The periodic sweep asks the same question a tick later, with the
|
||||
// registry filled in and the tree caught up, and that is soon enough:
|
||||
// nothing here is serving a request in the meantime.
|
||||
// The periodic sweep asks the same question a tick later, with the registry
|
||||
// filled in and the tree caught up, and that is soon enough: nothing here
|
||||
// is serving a request in the meantime.
|
||||
//
|
||||
// Off is not the same question. Then nothing on disk is worth keeping and
|
||||
// deleting it promptly is the setting's whole promise, so that one still
|
||||
// happens before anything else runs.
|
||||
if !crate::daemon::scrollback::enabled() {
|
||||
crate::daemon::scrollback::sweep(&std::collections::HashSet::new());
|
||||
}
|
||||
// A daemon that was killed outright never retired anything, so the files of
|
||||
// panes that died with it are still here. Their commands cannot be
|
||||
// recovered — the mark saying which were new belongs to a shell that is
|
||||
|
||||
@@ -38,13 +38,11 @@ struct Instance {
|
||||
|
||||
impl Instance {
|
||||
fn new() -> Instance {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("config.json"),
|
||||
r#"{"persist_scrollback": true}"#,
|
||||
)
|
||||
.unwrap();
|
||||
Instance { dir }
|
||||
// No config: keeping each pane's screen is what the daemon does, not
|
||||
// something it is asked to do.
|
||||
Instance {
|
||||
dir: tempfile::TempDir::new().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &std::path::Path {
|
||||
|
||||
@@ -2539,14 +2539,6 @@ impl Tty7App {
|
||||
self.update_config(cx, |cfg| cfg.restore_session = on);
|
||||
}
|
||||
|
||||
/// The daemon reads this from the config file on its own — it is the one
|
||||
/// holding the output — so there is nothing to tell it here. Turning it off
|
||||
/// also removes what was already stored, which the daemon does on its next
|
||||
/// pass rather than leaving the bytes behind.
|
||||
pub(crate) fn set_persist_scrollback(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
self.update_config(cx, |cfg| cfg.persist_scrollback = on);
|
||||
}
|
||||
|
||||
/// Takes effect on the next pane: a shell is told where its history lives
|
||||
/// when it starts, and nothing can move it afterwards.
|
||||
pub(crate) fn set_per_pane_history(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -1483,14 +1483,6 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
adds is written back when it closes, so nothing is lost. Applies to bash and zsh \
|
||||
panes that tty7 can set up; a shell started with your own arguments is left alone."
|
||||
}
|
||||
L10nKey::SettingsPersistScrollback => "Keep pane output on disk",
|
||||
L10nKey::SettingsPersistScrollbackDescription => {
|
||||
"If the background service dies without warning — a crash, or a reboot — panes come \
|
||||
back showing what was on them instead of blank. The processes are gone either way; \
|
||||
this restores the picture. It writes a capped tail of every pane's output to disk, \
|
||||
including anything printed there: tokens, the output of `env`, an agent's \
|
||||
transcript. Off means that output only ever lives in memory."
|
||||
}
|
||||
L10nKey::PanelMoreChangedFiles => {
|
||||
"… and {count} more changed files — run git diff to see them."
|
||||
}
|
||||
|
||||
@@ -1530,14 +1530,6 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
新しいペインは空ではなく既存の履歴から始まり、追加された分はペインを閉じるときに書き戻されるので失われません。\
|
||||
tty7 が設定できる bash と zsh のペインが対象で、独自の引数で起動したシェルはそのままです"
|
||||
}
|
||||
L10nKey::SettingsPersistScrollback => "ペインの出力をディスクに残す",
|
||||
L10nKey::SettingsPersistScrollbackDescription => {
|
||||
"バックグラウンドサービスが引き継ぎの間もなく落ちた場合(クラッシュや再起動)、\
|
||||
ペインは空ではなく、そこにあった内容を表示して戻ります。プロセスはいずれにせよ失われ、\
|
||||
ここで戻るのは画面だけです。各ペインの出力の末尾を上限つきでディスクに書き込みます。\
|
||||
そこに表示されたもの(トークン、`env` の出力、エージェントの記録)も含みます。\
|
||||
オフなら、その出力はメモリ上にしか存在しません。"
|
||||
}
|
||||
L10nKey::PanelMoreChangedFiles => {
|
||||
"… さらに変更されたファイル {count} 個 — 表示するには `git diff` を実行してください"
|
||||
}
|
||||
|
||||
@@ -1226,8 +1226,6 @@ l10n_keys! {
|
||||
PaneRestoredScreenBanner,
|
||||
AppRestartServerBodyInPlace,
|
||||
SettingsDaemonStaleDescInPlace,
|
||||
SettingsPersistScrollback,
|
||||
SettingsPersistScrollbackDescription,
|
||||
SettingsPerPaneHistory,
|
||||
SettingsPerPaneHistoryDescription,
|
||||
}
|
||||
|
||||
@@ -1406,12 +1406,6 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
新面板会从你已有的历史开始,而不是一片空白;面板关闭时,它新增的部分会写回原来的历史文件,不会丢。\
|
||||
只对 tty7 能接管的 bash 和 zsh 面板生效;用你自己参数启动的 shell 不受影响。"
|
||||
}
|
||||
L10nKey::SettingsPersistScrollback => "把面板输出留在磁盘上",
|
||||
L10nKey::SettingsPersistScrollbackDescription => {
|
||||
"后台服务如果没来得及交接就没了(崩溃、重启机器),面板回来时会显示原先的内容,而不是一片空白。\
|
||||
进程无论如何都救不回来,这里恢复的只是画面。它会把每个面板输出的末尾(有上限)写到磁盘上,\
|
||||
包括那里打印过的一切:token、`env` 的输出、agent 的对话记录。关掉则这些输出只存在于内存里。"
|
||||
}
|
||||
L10nKey::PanelMoreChangedFiles => "…还有 {count} 个变更文件——运行 git diff 查看。",
|
||||
L10nKey::PanelMoreChangedFiles => "…还有 {count} 个变更文件——运行 `git diff` 查看。",
|
||||
L10nKey::ScmFilesChanged => "{count} 个文件改动",
|
||||
|
||||
@@ -4743,7 +4743,6 @@ impl Tty7App {
|
||||
NewTabPosition::End => 1,
|
||||
};
|
||||
let restore_session = cfg.restore_session;
|
||||
let persist_scrollback = cfg.persist_scrollback;
|
||||
let remember_window_size = cfg.remember_window_size;
|
||||
let show_tray_icon = cfg.show_tray_icon;
|
||||
let tab_bar_idx = match cfg.tab_bar_position {
|
||||
@@ -4804,10 +4803,6 @@ impl Tty7App {
|
||||
.checked(restore_session)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_restore_session(*on, cx)))
|
||||
.into_any_element();
|
||||
let persist_scrollback_switch = crate::ui::theme::switch("wt-persist-scrollback", cx)
|
||||
.checked(persist_scrollback)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_persist_scrollback(*on, cx)))
|
||||
.into_any_element();
|
||||
let remember_window_switch = crate::ui::theme::switch("wt-remember-window", cx)
|
||||
.checked(remember_window_size)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_remember_window_size(*on, cx)))
|
||||
@@ -4901,12 +4896,6 @@ impl Tty7App {
|
||||
restore_switch,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
t(L10nKey::SettingsPersistScrollback),
|
||||
t(L10nKey::SettingsPersistScrollbackDescription),
|
||||
persist_scrollback_switch,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
t(L10nKey::SettingsShowTrayIcon),
|
||||
t(L10nKey::SettingsShowTrayIconDesc),
|
||||
|
||||
Reference in New Issue
Block a user