update: tuiからspawnする際にエラー詳細が落ちていた問題を修正
This commit is contained in:
parent
c693126703
commit
c214ea79d4
2
TODO.md
2
TODO.md
|
|
@ -11,7 +11,7 @@
|
||||||
- Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md)
|
- Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md)
|
||||||
- ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md)
|
- ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md)
|
||||||
- auto-kick 由来ターンが描画されない → [tickets/tui-pod-event-render.md](tickets/tui-pod-event-render.md)
|
- auto-kick 由来ターンが描画されない → [tickets/tui-pod-event-render.md](tickets/tui-pod-event-render.md)
|
||||||
- spawn 失敗時、Pod の stderr 末尾(同名衝突など)が TUI に出ず "pod exited before becoming ready" だけになる
|
- spawn 失敗時に Pod の stderr が TUI に表示されない → [tickets/tui-spawn-error-surface.md](tickets/tui-spawn-error-surface.md)
|
||||||
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
|
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
|
||||||
- メモリ機構
|
- メモリ機構
|
||||||
- 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
|
- 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ pub struct App {
|
||||||
pub current_tool: Option<String>,
|
pub current_tool: Option<String>,
|
||||||
pub input: InputBuffer,
|
pub input: InputBuffer,
|
||||||
pub quit: bool,
|
pub quit: bool,
|
||||||
pub shutdown_confirm: Option<std::time::Instant>,
|
|
||||||
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
|
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
|
||||||
/// records the instant; a second press within the timeout exits the
|
/// records the instant; a second press within the timeout exits the
|
||||||
/// TUI (the Pod itself stays alive).
|
/// TUI (the Pod itself stays alive).
|
||||||
|
|
@ -86,7 +85,6 @@ impl App {
|
||||||
current_tool: None,
|
current_tool: None,
|
||||||
input: InputBuffer::new(),
|
input: InputBuffer::new(),
|
||||||
quit: false,
|
quit: false,
|
||||||
shutdown_confirm: None,
|
|
||||||
quit_confirm: None,
|
quit_confirm: None,
|
||||||
blocks: Vec::new(),
|
blocks: Vec::new(),
|
||||||
scroll: Scroll::default(),
|
scroll: Scroll::default(),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ mod ui;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crossterm::event::{
|
use crossterm::event::{
|
||||||
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||||
|
|
@ -201,7 +200,7 @@ async fn run_attach(
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let socket_path = resolve_socket(&pod_name, socket_override);
|
let socket_path = resolve_socket(&pod_name, socket_override);
|
||||||
let mut terminal = enter_fullscreen()?;
|
let mut terminal = enter_fullscreen()?;
|
||||||
run(&mut terminal, pod_name, &socket_path, false).await
|
run(&mut terminal, pod_name, &socket_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
|
async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
@ -224,31 +223,18 @@ async fn run_spawn(resume_from: Option<SessionId>) -> Result<(), Box<dyn std::er
|
||||||
let SpawnReady {
|
let SpawnReady {
|
||||||
pod_name,
|
pod_name,
|
||||||
socket_path,
|
socket_path,
|
||||||
mut child,
|
|
||||||
stderr_drain,
|
|
||||||
} = ready;
|
} = ready;
|
||||||
|
|
||||||
let mut terminal = enter_fullscreen()?;
|
let mut terminal = enter_fullscreen()?;
|
||||||
let result = run(&mut terminal, pod_name, &socket_path, true).await;
|
let result = run(&mut terminal, pod_name, &socket_path).await;
|
||||||
|
|
||||||
// Leave alt-screen before reaping the child so any final pod stderr
|
// Leave alt-screen explicitly before `main`'s terminal restore path.
|
||||||
// (drained off-line by `stderr_drain`) cannot collide with the
|
|
||||||
// restored scrollback.
|
|
||||||
let _ = execute!(
|
let _ = execute!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut(),
|
||||||
DisableMouseCapture,
|
DisableMouseCapture,
|
||||||
LeaveAlternateScreen
|
LeaveAlternateScreen
|
||||||
);
|
);
|
||||||
|
|
||||||
match tokio::time::timeout(Duration::from_secs(3), child.wait()).await {
|
|
||||||
Ok(Ok(_)) => {}
|
|
||||||
_ => {
|
|
||||||
let _ = child.start_kill();
|
|
||||||
let _ = child.wait().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stderr_drain.abort();
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +250,6 @@ async fn run(
|
||||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||||
pod_name: String,
|
pod_name: String,
|
||||||
socket_path: &std::path::Path,
|
socket_path: &std::path::Path,
|
||||||
shutdown_pod_on_exit: bool,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut app = App::new(pod_name);
|
let mut app = App::new(pod_name);
|
||||||
|
|
||||||
|
|
@ -272,7 +257,7 @@ async fn run(
|
||||||
Ok(mut client) => {
|
Ok(mut client) => {
|
||||||
app.connected = true;
|
app.connected = true;
|
||||||
let _ = client.send(&Method::GetHistory).await;
|
let _ = client.send(&Method::GetHistory).await;
|
||||||
run_loop(terminal, &mut app, client, shutdown_pod_on_exit).await?;
|
run_loop(terminal, &mut app, client).await?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.push_error(format!(
|
app.push_error(format!(
|
||||||
|
|
@ -290,15 +275,11 @@ async fn run_loop(
|
||||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||||
app: &mut App,
|
app: &mut App,
|
||||||
mut client: PodClient,
|
mut client: PodClient,
|
||||||
shutdown_pod_on_exit: bool,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
terminal.draw(|f| ui::draw(f, app))?;
|
terminal.draw(|f| ui::draw(f, app))?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if app.quit {
|
if app.quit {
|
||||||
if shutdown_pod_on_exit {
|
|
||||||
let _ = client.send(&Method::Shutdown).await;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,10 +395,12 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||||
KeyCode::Char('x') if ctrl => Some(if app.running {
|
KeyCode::Char('x') if ctrl => Some(if app.running {
|
||||||
Some(Method::Cancel)
|
Some(Method::Cancel)
|
||||||
} else {
|
} else {
|
||||||
app.push_error("Nothing to cancel (Pod is not running).");
|
Some(Method::Shutdown)
|
||||||
None
|
|
||||||
}),
|
}),
|
||||||
KeyCode::Char('d') if ctrl => Some(handle_shutdown(app)),
|
KeyCode::Char('d') if ctrl => {
|
||||||
|
app.quit = true;
|
||||||
|
Some(None)
|
||||||
|
}
|
||||||
KeyCode::Enter if alt => {
|
KeyCode::Enter if alt => {
|
||||||
app.insert_newline();
|
app.insert_newline();
|
||||||
Some(app.refresh_completion())
|
Some(app.refresh_completion())
|
||||||
|
|
@ -550,21 +533,6 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||||
|
|
||||||
const CONFIRM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
const CONFIRM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||||
|
|
||||||
fn handle_shutdown(app: &mut App) -> Option<Method> {
|
|
||||||
if !app.running {
|
|
||||||
return Some(Method::Shutdown);
|
|
||||||
}
|
|
||||||
if let Some(t) = app.shutdown_confirm
|
|
||||||
&& t.elapsed() < CONFIRM_TIMEOUT
|
|
||||||
{
|
|
||||||
app.shutdown_confirm = None;
|
|
||||||
return Some(Method::Shutdown);
|
|
||||||
}
|
|
||||||
app.shutdown_confirm = Some(std::time::Instant::now());
|
|
||||||
app.push_error("Turn is running. Press Ctrl-D again to cancel and shut down.");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Running → send `Method::Pause`.
|
/// Running → send `Method::Pause`.
|
||||||
/// Idle / Paused → 2-tap to quit the TUI (the Pod keeps running).
|
/// Idle / Paused → 2-tap to quit the TUI (the Pod keeps running).
|
||||||
fn handle_pause_or_quit(app: &mut App) -> Option<Method> {
|
fn handle_pause_or_quit(app: &mut App) -> Option<Method> {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
//! Rendered at the user's current cursor position when `tui` is invoked
|
//! Rendered at the user's current cursor position when `tui` is invoked
|
||||||
//! with no positional argument. Walks the cwd for a `.insomnia/manifest.toml`
|
//! with no positional argument. Walks the cwd for a `.insomnia/manifest.toml`
|
||||||
//! to seed defaults, prompts for the Pod's name, and on confirmation
|
//! to seed defaults, prompts for the Pod's name, and on confirmation
|
||||||
//! launches the `pod` binary as a subprocess with a freshly built
|
//! launches the `pod` binary as an independent process with a freshly built
|
||||||
//! overlay (name + cwd scope when no project manifest exists). Once
|
//! overlay (name + cwd scope when no project manifest exists). Once
|
||||||
//! the child reports its socket via the `INSOMNIA-READY` stderr line,
|
//! the process reports its socket via the `INSOMNIA-READY` stderr line,
|
||||||
//! the dialog hands control back so main can switch the terminal to
|
//! the dialog hands control back so main can switch the terminal to
|
||||||
//! alternate-screen mode.
|
//! alternate-screen mode.
|
||||||
//!
|
//!
|
||||||
|
|
@ -29,9 +29,7 @@ use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::{Frame, TerminalOptions, Viewport};
|
use ratatui::{Frame, TerminalOptions, Viewport};
|
||||||
use session_store::SessionId;
|
use session_store::SessionId;
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::process::Command;
|
||||||
use tokio::process::{Child, Command};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
|
|
||||||
const READY_PREFIX: &str = "INSOMNIA-READY\t";
|
const READY_PREFIX: &str = "INSOMNIA-READY\t";
|
||||||
const VIEWPORT_LINES: u16 = 6;
|
const VIEWPORT_LINES: u16 = 6;
|
||||||
|
|
@ -40,8 +38,6 @@ const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
pub struct SpawnReady {
|
pub struct SpawnReady {
|
||||||
pub pod_name: String,
|
pub pod_name: String,
|
||||||
pub socket_path: PathBuf,
|
pub socket_path: PathBuf,
|
||||||
pub child: Child,
|
|
||||||
pub stderr_drain: JoinHandle<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum SpawnOutcome {
|
pub enum SpawnOutcome {
|
||||||
|
|
@ -290,6 +286,16 @@ async fn wait_for_ready(
|
||||||
let pod_bin = resolve_pod_command();
|
let pod_bin = resolve_pod_command();
|
||||||
let cwd = std::env::current_dir().map_err(SpawnError::Io)?;
|
let cwd = std::env::current_dir().map_err(SpawnError::Io)?;
|
||||||
|
|
||||||
|
let pod_runtime_dir = manifest::paths::pod_runtime_dir(&form.name).ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"could not resolve runtime directory (set INSOMNIA_HOME, INSOMNIA_RUNTIME_DIR, XDG_RUNTIME_DIR, or HOME)",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
std::fs::create_dir_all(&pod_runtime_dir).map_err(SpawnError::Io)?;
|
||||||
|
let stderr_path = pod_runtime_dir.join("stderr.log");
|
||||||
|
let stderr_file = std::fs::File::create(&stderr_path).map_err(SpawnError::Io)?;
|
||||||
|
|
||||||
let mut command = Command::new(&pod_bin);
|
let mut command = Command::new(&pod_bin);
|
||||||
command
|
command
|
||||||
.arg("--overlay")
|
.arg("--overlay")
|
||||||
|
|
@ -297,77 +303,151 @@ async fn wait_for_ready(
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::from(stderr_file))
|
||||||
.kill_on_drop(true);
|
.process_group(0);
|
||||||
if let Some(id) = form.resume_from {
|
if let Some(id) = form.resume_from {
|
||||||
command.arg("--session").arg(id.to_string());
|
command.arg("--session").arg(id.to_string());
|
||||||
}
|
}
|
||||||
let mut child = command.spawn().map_err(SpawnError::PodLaunchFailed)?;
|
let mut child = command.spawn().map_err(SpawnError::PodLaunchFailed)?;
|
||||||
|
|
||||||
let stderr = child
|
// Default `kill_on_drop = false` plus `process_group(0)` makes this
|
||||||
.stderr
|
// a detached Pod for TUI lifecycle purposes once startup succeeds:
|
||||||
.take()
|
// dropping the handle does not terminate it, and terminal-generated
|
||||||
.expect("stderr is piped; take() must succeed");
|
// signals for the TUI's process group do not hit the Pod. Runtime
|
||||||
let mut reader = BufReader::new(stderr).lines();
|
// state/socket files are the source of truth after that point.
|
||||||
let mut tail = StderrTail::new();
|
let ready = match wait_for_ready_file(terminal, form, &stderr_path, &mut child).await {
|
||||||
|
Ok(ready) => ready,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = child.start_kill();
|
||||||
|
let _ = child.wait().await;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = child.wait().await;
|
||||||
|
});
|
||||||
|
Ok(ready)
|
||||||
|
}
|
||||||
|
|
||||||
let timeout = tokio::time::sleep(READY_TIMEOUT);
|
async fn wait_for_ready_file(
|
||||||
tokio::pin!(timeout);
|
terminal: &mut InlineTerminal,
|
||||||
|
form: &mut Form,
|
||||||
|
stderr_path: &std::path::Path,
|
||||||
|
child: &mut tokio::process::Child,
|
||||||
|
) -> Result<SpawnReady, SpawnError> {
|
||||||
|
let mut tail = StderrTail::new();
|
||||||
|
let deadline = tokio::time::Instant::now() + READY_TIMEOUT;
|
||||||
|
let mut offset = 0usize;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
let content = match tokio::fs::read_to_string(stderr_path).await {
|
||||||
line = reader.next_line() => {
|
Ok(content) => content,
|
||||||
match line {
|
Err(e) if e.kind() == io::ErrorKind::NotFound => String::new(),
|
||||||
Ok(Some(line)) => {
|
Err(e) => return Err(SpawnError::Io(e)),
|
||||||
if let Some(rest) = line.strip_prefix(READY_PREFIX) {
|
};
|
||||||
let mut parts = rest.splitn(2, '\t');
|
if content.len() > offset {
|
||||||
let pod_name = parts.next().unwrap_or("").to_string();
|
for line in content[offset..].lines() {
|
||||||
let socket_str = parts.next().unwrap_or("").to_string();
|
if let Some(rest) = line.strip_prefix(READY_PREFIX) {
|
||||||
if pod_name.is_empty() || socket_str.is_empty() {
|
let mut parts = rest.splitn(2, '\t');
|
||||||
return Err(SpawnError::PodExitedEarly {
|
let pod_name = parts.next().unwrap_or("").to_string();
|
||||||
stderr_tail: format!("malformed ready line: {line}"),
|
let socket_str = parts.next().unwrap_or("").to_string();
|
||||||
});
|
if pod_name.is_empty() || socket_str.is_empty() {
|
||||||
}
|
|
||||||
let socket_path = PathBuf::from(socket_str);
|
|
||||||
|
|
||||||
let stderr_drain = tokio::spawn(async move {
|
|
||||||
while let Ok(Some(_)) = reader.next_line().await {}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(SpawnReady {
|
|
||||||
pod_name,
|
|
||||||
socket_path,
|
|
||||||
child,
|
|
||||||
stderr_drain,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
tail.push(&line);
|
|
||||||
form.message = Some((line, MessageKind::Progress));
|
|
||||||
let _ = terminal.draw(|f| draw_form(f, form));
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
let _ = child.wait().await;
|
|
||||||
return Err(SpawnError::PodExitedEarly {
|
return Err(SpawnError::PodExitedEarly {
|
||||||
stderr_tail: tail.into_string(),
|
stderr_tail: format!("malformed ready line: {line}"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => return Err(SpawnError::Io(e)),
|
let socket_path = PathBuf::from(socket_str);
|
||||||
|
wait_for_socket(
|
||||||
|
&socket_path,
|
||||||
|
deadline,
|
||||||
|
child,
|
||||||
|
stderr_path,
|
||||||
|
&mut tail,
|
||||||
|
&mut offset,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(SpawnReady {
|
||||||
|
pod_name,
|
||||||
|
socket_path,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
tail.push(line);
|
||||||
|
form.message = Some((line.to_string(), MessageKind::Progress));
|
||||||
|
let _ = terminal.draw(|f| draw_form(f, form));
|
||||||
}
|
}
|
||||||
|
offset = content.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokio::time::Instant::now() >= deadline {
|
||||||
|
return Err(SpawnError::Timeout);
|
||||||
|
}
|
||||||
|
tokio::select! {
|
||||||
status = child.wait() => {
|
status = child.wait() => {
|
||||||
let _ = status;
|
let _ = status;
|
||||||
|
// Pod は exit 直前に最終 stderr 行を flush することがある。
|
||||||
|
// child.wait() が解決した後に再読みして、原因行を取りこ
|
||||||
|
// ぼさず PodExitedEarly に載せる。
|
||||||
|
drain_stderr_into_tail(stderr_path, &mut tail, &mut offset).await;
|
||||||
return Err(SpawnError::PodExitedEarly {
|
return Err(SpawnError::PodExitedEarly {
|
||||||
stderr_tail: tail.into_string(),
|
stderr_tail: tail.into_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ = &mut timeout => {
|
_ = tokio::time::sleep(Duration::from_millis(100)) => {}
|
||||||
let _ = child.start_kill();
|
|
||||||
return Err(SpawnError::Timeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_for_socket(
|
||||||
|
socket_path: &std::path::Path,
|
||||||
|
deadline: tokio::time::Instant,
|
||||||
|
child: &mut tokio::process::Child,
|
||||||
|
stderr_path: &std::path::Path,
|
||||||
|
tail: &mut StderrTail,
|
||||||
|
offset: &mut usize,
|
||||||
|
) -> Result<(), SpawnError> {
|
||||||
|
loop {
|
||||||
|
match tokio::net::UnixStream::connect(socket_path).await {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(e)
|
||||||
|
if e.kind() == io::ErrorKind::NotFound
|
||||||
|
|| e.kind() == io::ErrorKind::ConnectionRefused => {}
|
||||||
|
Err(e) => return Err(SpawnError::Io(e)),
|
||||||
|
}
|
||||||
|
if tokio::time::Instant::now() >= deadline {
|
||||||
|
return Err(SpawnError::Timeout);
|
||||||
|
}
|
||||||
|
tokio::select! {
|
||||||
|
status = child.wait() => {
|
||||||
|
let _ = status;
|
||||||
|
drain_stderr_into_tail(stderr_path, tail, offset).await;
|
||||||
|
return Err(SpawnError::PodExitedEarly {
|
||||||
|
stderr_tail: tail.as_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(Duration::from_millis(50)) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn drain_stderr_into_tail(
|
||||||
|
stderr_path: &std::path::Path,
|
||||||
|
tail: &mut StderrTail,
|
||||||
|
offset: &mut usize,
|
||||||
|
) {
|
||||||
|
let Ok(content) = tokio::fs::read_to_string(stderr_path).await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if content.len() <= *offset {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for line in content[*offset..].lines() {
|
||||||
|
if !line.starts_with(READY_PREFIX) {
|
||||||
|
tail.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*offset = content.len();
|
||||||
|
}
|
||||||
|
|
||||||
fn build_overlay_toml(form: &Form) -> String {
|
fn build_overlay_toml(form: &Form) -> String {
|
||||||
let mut root = toml::value::Table::new();
|
let mut root = toml::value::Table::new();
|
||||||
|
|
||||||
|
|
@ -449,6 +529,9 @@ impl StderrTail {
|
||||||
}
|
}
|
||||||
self.lines.push_back(line.to_string());
|
self.lines.push_back(line.to_string());
|
||||||
}
|
}
|
||||||
|
fn as_string(&self) -> String {
|
||||||
|
self.lines.iter().cloned().collect::<Vec<_>>().join(" | ")
|
||||||
|
}
|
||||||
fn into_string(self) -> String {
|
fn into_string(self) -> String {
|
||||||
self.lines.into_iter().collect::<Vec<_>>().join(" | ")
|
self.lines.into_iter().collect::<Vec<_>>().join(" | ")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,32 +90,34 @@ Paused 中に Enter すると、入力の有無で 2 通り:
|
||||||
|
|
||||||
| キー | Running 中 | Idle / Paused |
|
| キー | Running 中 | Idle / Paused |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `Ctrl-X` | `Method::Cancel`(進行中ターンを破棄 → Idle) | no-op(エラー表示のみ) |
|
| `Ctrl-X` | `Method::Cancel`(進行中ターンを破棄 → Idle) | `Method::Shutdown`(Pod を終了) |
|
||||||
| `Ctrl-C` | `Method::Pause`(進行中ターンを中断 → Paused) | 1 回目 warn、3 秒以内の 2 回目で TUI 終了(Pod は残る) |
|
| `Ctrl-C` | `Method::Pause`(進行中ターンを中断 → Paused) | 1 回目 warn、3 秒以内の 2 回目で TUI 終了(Pod は残る) |
|
||||||
| `Ctrl-D` | 1 回目 warn、3 秒以内の 2 回目で `Method::Shutdown` | `Method::Shutdown`(Pod を終了) |
|
| `Ctrl-D` | TUI 終了(Pod は残る、Pause しない) | TUI 終了(Pod は残る) |
|
||||||
|
|
||||||
### Cancel と Pause の違い
|
### Cancel と Pause の違い
|
||||||
|
|
||||||
- **Cancel** は「ターンを捨てる」: 進行中の LLM リクエスト・未完了 tool を打ち切り、状態は Idle。続きは Resume できない
|
- **Cancel** は「ターンを捨てる」: 進行中の LLM リクエスト・未完了 tool を打ち切り、状態は Idle。続きは Resume できない
|
||||||
- **Pause** は「止めるけど続けられるように」: 同じく打ち切るが状態は Paused、空 Enter で Resume 可能
|
- **Pause** は「止めるけど続けられるように」: 同じく打ち切るが状態は Paused、空 Enter で Resume 可能
|
||||||
|
|
||||||
Running 中に割り込みたい場合、ほとんどのケースで `Ctrl-C`(Pause)が自然。Ctrl-X(Cancel)は明示的に破棄したい時(LLM が暴走した時など)用。
|
Running 中に割り込みたい場合、ほとんどのケースで `Ctrl-C`(Pause)が自然。Ctrl-X(Cancel)は明示的に破棄したい時(LLM が暴走した時など)用。Pod を終了したい場合は、先に Running ではない状態(Idle / Paused)にしてから `Ctrl-X` で Shutdown する。
|
||||||
|
|
||||||
### Ctrl-C と Ctrl-D の 2 段階 UX
|
### Ctrl-C と Ctrl-D の終了 UX
|
||||||
|
|
||||||
どちらも「破壊的に見える操作」は確認を挟む:
|
|
||||||
|
|
||||||
|
- Ctrl-X Running 中: `Method::Cancel`。終了したい場合は、明示的にターンを止めて Idle に戻してからもう一度 `Ctrl-X`
|
||||||
|
- Ctrl-X Idle / Paused: `Method::Shutdown` を送って Pod を終了
|
||||||
- Ctrl-C Running 中: 1 回目で即 Pause(破壊的ではない)
|
- Ctrl-C Running 中: 1 回目で即 Pause(破壊的ではない)
|
||||||
- Ctrl-C Idle / Paused: 1 回目で warn メッセージ、3 秒以内の 2 回目で TUI 終了(Pod は残る)
|
- Ctrl-C Idle / Paused: 1 回目で warn メッセージ、3 秒以内の 2 回目で TUI 終了(Pod は残る)
|
||||||
- Ctrl-D Running 中: 1 回目で warn、3 秒以内の 2 回目で Shutdown
|
- Ctrl-D: 状態に関わらず即 TUI 終了(Pod は残る)。Running 中でも Pause / Cancel / Shutdown は送らない
|
||||||
- Ctrl-D Idle / Paused: 1 回目で即 Shutdown
|
|
||||||
|
|
||||||
`Ctrl-C` は Pod は落とさず TUI プロセスだけ抜ける。`Ctrl-D` は Pod 自体に `Method::Shutdown` を送って終了させる(Pod プロセスが消える)。
|
`Ctrl-X` は Running 中だけ Cancel、Idle / Paused では Shutdown。`Ctrl-C` は Running 中だけ Pod に `Method::Pause` を送り、それ以外では Pod は落とさず TUI プロセスだけ抜ける。`Ctrl-D` は常に Pod へ制御メソッドを送らず TUI プロセスだけ抜ける。
|
||||||
|
|
||||||
|
TUI のダイアログから Pod を起動する経路では、起動した Pod は TUI の子プロセスとして管理・終了されず、独立したプロセスとして残る。TUI 終了後は `tui <pod-name>` で再接続できる。
|
||||||
|
|
||||||
## 履歴メモ
|
## 履歴メモ
|
||||||
|
|
||||||
- かつて存在した `Ctrl-R`(Resume 専用)は、空 Enter での Resume に統合されたため廃止
|
- かつて存在した `Ctrl-R`(Resume 専用)は、空 Enter での Resume に統合されたため廃止
|
||||||
- かつて存在した `Esc`(TUI 終了)は、`Ctrl-C` の 2 連打 UX に統合されたため廃止
|
- かつて存在した `Esc`(TUI 終了)は、`Ctrl-C` の 2 連打 UX に統合されたため廃止
|
||||||
|
- かつて `Ctrl-D` は Pod に `Method::Shutdown` を送っていたが、TUI だけを抜けるデタッチ操作に変更された
|
||||||
- 旧 inline viewport 時代は履歴スクロールを端末側スクロールバックに
|
- 旧 inline viewport 時代は履歴スクロールを端末側スクロールバックに
|
||||||
任せていたため TUI 内のスクロールキーは存在しなかった。全画面 alt screen
|
任せていたため TUI 内のスクロールキーは存在しなかった。全画面 alt screen
|
||||||
への移行(`tickets/tui-fullscreen-overhaul.md`)で `Shift-Up/Down` ほか
|
への移行(`tickets/tui-fullscreen-overhaul.md`)で `Shift-Up/Down` ほか
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user