tuiの補完の実装
This commit is contained in:
@@ -28,15 +28,7 @@ use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use serde::Deserialize;
|
||||
use tools::ScopedFs;
|
||||
|
||||
/// A file the compact worker has marked for auto-read in the new session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ReadRequirement {
|
||||
pub path: PathBuf,
|
||||
/// 0-based line offset. `None` means from the start of the file.
|
||||
pub offset: Option<usize>,
|
||||
/// Maximum number of lines. `None` means to the end of the file.
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
use crate::fs_view::{ReadRequirement, slice_lines};
|
||||
|
||||
/// Aggregated output of a compact worker run.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@@ -281,18 +273,6 @@ fn estimate_tokens(bytes: usize) -> u64 {
|
||||
(bytes as u64).div_ceil(4)
|
||||
}
|
||||
|
||||
/// Return the slice of `text` covered by `offset` (line index) and
|
||||
/// optional `limit` (line count), preserving the original newline
|
||||
/// separation. Returns the whole file when both defaults apply.
|
||||
pub(crate) fn slice_lines(text: &str, offset: usize, limit: Option<usize>) -> String {
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let start = offset.min(lines.len());
|
||||
let end = limit
|
||||
.map(|n| start.saturating_add(n).min(lines.len()))
|
||||
.unwrap_or(lines.len());
|
||||
lines[start..end].join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -108,6 +108,10 @@ impl PodController {
|
||||
// can emit typed lifecycle `Event`s (currently: compact progress).
|
||||
pod.attach_event_tx(event_tx.clone());
|
||||
|
||||
// Stashed during tool registration below so we can attach a
|
||||
// `PodFsView` to the shared state once the latter exists.
|
||||
let fs_for_view: tools::ScopedFs;
|
||||
|
||||
// Register event bridge callbacks on the worker
|
||||
{
|
||||
let worker = pod.worker_mut();
|
||||
@@ -226,6 +230,10 @@ impl PodController {
|
||||
// touching.
|
||||
let fs = tools::ScopedFs::new(scope_for_tools, pwd_for_tools.clone());
|
||||
let tracker = tools::Tracker::new();
|
||||
// The same ScopedFs also powers the IPC `ListCompletions`
|
||||
// query — keep a clone for the FS view we attach below,
|
||||
// since the tools consume `fs` itself.
|
||||
fs_for_view = fs.clone();
|
||||
worker.register_tools(tools::builtin_tools(fs, tracker.clone()));
|
||||
|
||||
// Memory subsystem opt-in. When `[memory]` is present in
|
||||
@@ -278,6 +286,7 @@ impl PodController {
|
||||
));
|
||||
shared_state.update_history(pod.worker().history().to_vec());
|
||||
shared_state.set_user_segments(pod.user_segments().to_vec());
|
||||
shared_state.set_fs_view(crate::fs_view::PodFsView::new(fs_for_view));
|
||||
runtime_dir.write_manifest(&manifest_toml).await?;
|
||||
runtime_dir.write_status(&shared_state).await?;
|
||||
runtime_dir.write_history(&shared_state).await?;
|
||||
@@ -527,9 +536,10 @@ impl PodController {
|
||||
break;
|
||||
}
|
||||
|
||||
// GetHistory is handled at the socket layer (direct response).
|
||||
// If it somehow reaches the controller, ignore it.
|
||||
Method::GetHistory => {}
|
||||
// GetHistory / ListCompletions are handled at the socket
|
||||
// layer (direct response). If they somehow reach the
|
||||
// controller, ignore them.
|
||||
Method::GetHistory | Method::ListCompletions { .. } => {}
|
||||
|
||||
Method::PodEvent(event) => {
|
||||
// (1) system side effects — idempotent and
|
||||
@@ -728,7 +738,7 @@ where
|
||||
// drain it at its next pre_llm_request.
|
||||
notify_buffer.push(message);
|
||||
}
|
||||
Some(Method::GetHistory) => {}
|
||||
Some(Method::GetHistory | Method::ListCompletions { .. }) => {}
|
||||
Some(Method::PodEvent(event)) => {
|
||||
// mpsc is consume-once, so we cannot defer this
|
||||
// to the next main-loop iteration — drop here
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//! Pod 視点のファイルシステム操作。
|
||||
//!
|
||||
//! `ScopedFs` の上に「Pod が読み取りたい / 列挙したい」操作を集約する軽い wrapper。
|
||||
//!
|
||||
//! - `ReadRequirement` と `render_auto_read` — compact worker が `mark_read_required`
|
||||
//! で nominate したファイルを再読し、`[Auto-read file: ...]` system message に
|
||||
//! 変換する経路。`Pod::compact` から呼ばれる。
|
||||
//! - `slice_lines` — 行 offset / limit でテキストを切り出す純粋ヘルパ。
|
||||
//! compact tool 側の `mark_read_required` でも使用。
|
||||
//! - `list_file_completions` — TUI 補完用、prefix マッチでファイル候補を列挙する経路。
|
||||
//! IPC `Method::ListCompletions` 経由で呼ばれる前提(Phase 2 で接続)。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use llm_worker::Item;
|
||||
use tools::ScopedFs;
|
||||
use tracing::warn;
|
||||
|
||||
/// 補完候補1件の最大数。`list_file_completions` がこの値を超えたら打ち切り。
|
||||
const COMPLETION_LIMIT: usize = 100;
|
||||
|
||||
/// Compact worker が `mark_read_required` で nominate した「次セッション開始時に
|
||||
/// 自動で再読すべきファイル」のエントリ。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReadRequirement {
|
||||
pub path: PathBuf,
|
||||
/// 0-based line offset. `None` means from the start of the file.
|
||||
pub offset: Option<usize>,
|
||||
/// Maximum number of lines. `None` means to the end of the file.
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// Pod から見えるファイルシステム操作の入口。Clone は cheap(`ScopedFs` 内 `Arc`)。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PodFsView {
|
||||
fs: ScopedFs,
|
||||
}
|
||||
|
||||
/// `list_file_completions` が返す候補1件。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FileCandidate {
|
||||
/// 入力 prefix と整合する形のパス(prefix が absolute なら absolute、
|
||||
/// relative なら pwd 相対)。
|
||||
pub path: String,
|
||||
pub is_dir: bool,
|
||||
}
|
||||
|
||||
impl PodFsView {
|
||||
pub fn new(fs: ScopedFs) -> Self {
|
||||
Self { fs }
|
||||
}
|
||||
|
||||
pub fn fs(&self) -> &ScopedFs {
|
||||
&self.fs
|
||||
}
|
||||
|
||||
/// `requirements` の各エントリを `ScopedFs` 経由で再読し、
|
||||
/// `[Auto-read file: <path>:<range>]\n<body>` 形式の system message に変換する。
|
||||
/// 読み取り失敗(NotFound / OutOfScope 等)は warn で記録してスキップする
|
||||
/// — compact 全体を落とさないため。
|
||||
pub fn render_auto_read(&self, requirements: &[ReadRequirement]) -> Vec<Item> {
|
||||
let mut out = Vec::with_capacity(requirements.len());
|
||||
for req in requirements {
|
||||
match self.fs.read_bytes(&req.path) {
|
||||
Ok(bytes) => {
|
||||
let text = String::from_utf8_lossy(&bytes).into_owned();
|
||||
let body = slice_lines(&text, req.offset.unwrap_or(0), req.limit);
|
||||
let range = format_range(req.offset, req.limit);
|
||||
out.push(Item::system_message(format!(
|
||||
"[Auto-read file: {}{range}]\n{body}",
|
||||
req.path.display()
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
path = %req.path.display(),
|
||||
error = %e,
|
||||
"auto-read target could not be read; skipping",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `prefix` にマッチするファイル / ディレクトリを scope 内で浅く列挙する。
|
||||
///
|
||||
/// - `prefix` が空 or `pwd` 相対のときは pwd 直下を見る
|
||||
/// - `prefix` が末尾 `/` のときはそのディレクトリ直下を全列挙
|
||||
/// - 末尾が名前部分のときは、その名前を starts_with でフィルタ
|
||||
/// - scope 上 readable なエントリのみ返す
|
||||
/// - ディレクトリ → ファイル の順、各グループ内は名前昇順
|
||||
/// - 上限 `COMPLETION_LIMIT` 件で打ち切り(深い列挙はしない)
|
||||
pub fn list_file_completions(&self, prefix: &str) -> Vec<FileCandidate> {
|
||||
let pwd = self.fs.pwd();
|
||||
let scope = self.fs.scope();
|
||||
let (dir, name_prefix, is_absolute) = split_prefix(prefix, pwd);
|
||||
|
||||
let read_dir = match std::fs::read_dir(&dir) {
|
||||
Ok(rd) => rd,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut out = Vec::new();
|
||||
for entry in read_dir.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy();
|
||||
if !name.starts_with(&name_prefix) {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if !scope.is_readable(&path) {
|
||||
continue;
|
||||
}
|
||||
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
|
||||
let display = if is_absolute {
|
||||
path.display().to_string()
|
||||
} else {
|
||||
path.strip_prefix(pwd)
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|_| path.display().to_string())
|
||||
};
|
||||
out.push(FileCandidate {
|
||||
path: display,
|
||||
is_dir,
|
||||
});
|
||||
}
|
||||
|
||||
out.sort_by(|a, b| match (a.is_dir, b.is_dir) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.path.cmp(&b.path),
|
||||
});
|
||||
out.truncate(COMPLETION_LIMIT);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// `text` の `offset` 行目から `limit` 行(None なら末尾まで)を、元の改行で繋いで返す。
|
||||
pub fn slice_lines(text: &str, offset: usize, limit: Option<usize>) -> String {
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let start = offset.min(lines.len());
|
||||
let end = limit
|
||||
.map(|n| start.saturating_add(n).min(lines.len()))
|
||||
.unwrap_or(lines.len());
|
||||
lines[start..end].join("\n")
|
||||
}
|
||||
|
||||
fn format_range(offset: Option<usize>, limit: Option<usize>) -> String {
|
||||
match (offset, limit) {
|
||||
(None, None) => String::new(),
|
||||
(Some(off), None) => format!(":{}-", off + 1),
|
||||
(None, Some(lim)) => format!(":1-{lim}"),
|
||||
(Some(off), Some(lim)) => format!(":{}-{}", off + 1, off.saturating_add(lim)),
|
||||
}
|
||||
}
|
||||
|
||||
fn split_prefix(prefix: &str, pwd: &Path) -> (PathBuf, String, bool) {
|
||||
let is_absolute = Path::new(prefix).is_absolute();
|
||||
let p = Path::new(prefix);
|
||||
let (parent, name) = if prefix.is_empty() || prefix.ends_with('/') {
|
||||
(p.to_path_buf(), String::new())
|
||||
} else {
|
||||
let parent = p.parent().map(|p| p.to_path_buf()).unwrap_or_default();
|
||||
let name = p
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
(parent, name)
|
||||
};
|
||||
let dir = if is_absolute {
|
||||
parent
|
||||
} else if parent.as_os_str().is_empty() {
|
||||
pwd.to_path_buf()
|
||||
} else {
|
||||
pwd.join(parent)
|
||||
};
|
||||
(dir, name, is_absolute)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use manifest::{Permission, Scope, ScopeConfig, ScopeRule};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn fs_for(dir: &TempDir) -> ScopedFs {
|
||||
ScopedFs::new(
|
||||
Scope::writable(dir.path()).unwrap(),
|
||||
dir.path().to_path_buf(),
|
||||
)
|
||||
}
|
||||
|
||||
fn touch(path: &Path, content: &str) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
std::fs::write(path, content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slice_lines_handles_offset_and_limit() {
|
||||
let text = "a\nb\nc\nd";
|
||||
assert_eq!(slice_lines(text, 0, None), "a\nb\nc\nd");
|
||||
assert_eq!(slice_lines(text, 1, Some(2)), "b\nc");
|
||||
assert_eq!(slice_lines(text, 10, None), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_auto_read_emits_system_messages_with_range_label() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let file = dir.path().join("hello.txt");
|
||||
std::fs::write(&file, "alpha\nbeta\ngamma\n").unwrap();
|
||||
|
||||
let view = PodFsView::new(fs_for(&dir));
|
||||
let items = view.render_auto_read(&[ReadRequirement {
|
||||
path: file.clone(),
|
||||
offset: Some(1),
|
||||
limit: Some(1),
|
||||
}]);
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
let rendered = format!("{:?}", items[0]);
|
||||
assert!(rendered.contains("Auto-read file"));
|
||||
assert!(rendered.contains(":2-2"));
|
||||
assert!(rendered.contains("beta"));
|
||||
assert!(!rendered.contains("alpha"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_auto_read_skips_unreadable_targets() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let view = PodFsView::new(fs_for(&dir));
|
||||
let items = view.render_auto_read(&[ReadRequirement {
|
||||
path: dir.path().join("missing.txt"),
|
||||
offset: None,
|
||||
limit: None,
|
||||
}]);
|
||||
assert!(items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_file_completions_lists_pwd_when_prefix_empty() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
touch(&dir.path().join("alpha.rs"), "");
|
||||
touch(&dir.path().join("beta.rs"), "");
|
||||
std::fs::create_dir(dir.path().join("subdir")).unwrap();
|
||||
let view = PodFsView::new(fs_for(&dir));
|
||||
|
||||
let cands = view.list_file_completions("");
|
||||
// ディレクトリ first
|
||||
let names: Vec<&str> = cands.iter().map(|c| c.path.as_str()).collect();
|
||||
assert_eq!(names, vec!["subdir", "alpha.rs", "beta.rs"]);
|
||||
assert!(cands[0].is_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_file_completions_filters_by_name_prefix() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
touch(&dir.path().join("alpha.rs"), "");
|
||||
touch(&dir.path().join("beta.rs"), "");
|
||||
let view = PodFsView::new(fs_for(&dir));
|
||||
|
||||
let cands = view.list_file_completions("al");
|
||||
assert_eq!(cands.len(), 1);
|
||||
assert_eq!(cands[0].path, "alpha.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_file_completions_descends_into_subdir_with_trailing_slash() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
touch(&dir.path().join("sub/x.rs"), "");
|
||||
touch(&dir.path().join("sub/y.rs"), "");
|
||||
let view = PodFsView::new(fs_for(&dir));
|
||||
|
||||
let cands = view.list_file_completions("sub/");
|
||||
let names: Vec<&str> = cands.iter().map(|c| c.path.as_str()).collect();
|
||||
assert_eq!(names, vec!["sub/x.rs", "sub/y.rs"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_file_completions_filters_out_non_readable_under_scope() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let secret = dir.path().join("secret");
|
||||
std::fs::create_dir(&secret).unwrap();
|
||||
touch(&dir.path().join("visible.rs"), "");
|
||||
touch(&secret.join("hidden.rs"), "");
|
||||
|
||||
let cfg = ScopeConfig {
|
||||
allow: vec![ScopeRule {
|
||||
target: dir.path().to_path_buf(),
|
||||
permission: Permission::Write,
|
||||
recursive: true,
|
||||
}],
|
||||
deny: vec![ScopeRule {
|
||||
target: secret.clone(),
|
||||
permission: Permission::Read,
|
||||
recursive: true,
|
||||
}],
|
||||
};
|
||||
let scope = Scope::from_config(&cfg).unwrap();
|
||||
let fs = ScopedFs::new(scope, dir.path().to_path_buf());
|
||||
let view = PodFsView::new(fs);
|
||||
|
||||
let cands = view.list_file_completions("");
|
||||
let names: Vec<&str> = cands.iter().map(|c| c.path.as_str()).collect();
|
||||
assert!(names.contains(&"visible.rs"));
|
||||
assert!(!names.contains(&"secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_file_completions_supports_absolute_prefix() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
touch(&dir.path().join("a.rs"), "");
|
||||
let view = PodFsView::new(fs_for(&dir));
|
||||
|
||||
let prefix = format!("{}/", dir.path().display());
|
||||
let cands = view.list_file_completions(&prefix);
|
||||
assert_eq!(cands.len(), 1);
|
||||
assert!(cands[0].path.starts_with('/'));
|
||||
assert!(cands[0].path.ends_with("a.rs"));
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,33 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) {
|
||||
// Client methods → handle or forward to controller
|
||||
method = reader.next::<Method>() => {
|
||||
match method {
|
||||
Ok(Some(Method::ListCompletions { kind, prefix })) => {
|
||||
let entries = match kind {
|
||||
protocol::CompletionKind::File => handle
|
||||
.shared_state
|
||||
.fs_view()
|
||||
.map(|view| view.list_file_completions(&prefix))
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|c| protocol::CompletionEntry {
|
||||
value: c.path,
|
||||
is_dir: c.is_dir,
|
||||
})
|
||||
.collect(),
|
||||
// Knowledge / Workflow resolvers are not wired
|
||||
// up yet — reply empty so the TUI sees a
|
||||
// consistent shape regardless of kind.
|
||||
protocol::CompletionKind::Knowledge
|
||||
| protocol::CompletionKind::Workflow => Vec::new(),
|
||||
};
|
||||
if writer
|
||||
.write(&Event::Completions { kind, entries })
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Some(Method::GetHistory)) => {
|
||||
let items = handle.shared_state.history();
|
||||
let segments_per_user = handle.shared_state.user_segments();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod compact;
|
||||
pub mod controller;
|
||||
pub mod fs_view;
|
||||
pub mod hook;
|
||||
pub mod ipc;
|
||||
pub mod prompt;
|
||||
|
||||
+6
-31
@@ -970,8 +970,9 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
|
||||
use crate::compact::worker::{
|
||||
CompactWorkerContext, CompactWorkerInterceptor, add_reference_tool,
|
||||
mark_read_required_tool, slice_lines, write_summary_tool,
|
||||
mark_read_required_tool, write_summary_tool,
|
||||
};
|
||||
use crate::fs_view::PodFsView;
|
||||
|
||||
// Decide the cut point by projecting the UsageRecord timeline onto
|
||||
// the current history: keep the tail whose estimated token count is
|
||||
@@ -1097,38 +1098,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
.clone()
|
||||
.ok_or(PodError::CompactSummaryMissing)?;
|
||||
|
||||
// Re-read each auto-read target through ScopedFs and render the
|
||||
// requested slice. Errors are logged and skipped rather than
|
||||
// Re-read each auto-read target via the Pod FS view. Errors are
|
||||
// logged and skipped inside `render_auto_read` rather than
|
||||
// aborting compaction — a missing / moved file should not fail
|
||||
// the whole compact.
|
||||
let mut auto_read_messages = Vec::new();
|
||||
for req in &final_ctx.read_required {
|
||||
match scoped_fs.read_bytes(&req.path) {
|
||||
Ok(bytes) => {
|
||||
let text = String::from_utf8_lossy(&bytes).into_owned();
|
||||
let body = slice_lines(&text, req.offset.unwrap_or(0), req.limit);
|
||||
let range = match (req.offset, req.limit) {
|
||||
(None, None) => String::new(),
|
||||
(Some(off), None) => format!(":{}-", off + 1),
|
||||
(None, Some(lim)) => format!(":1-{lim}"),
|
||||
(Some(off), Some(lim)) => {
|
||||
format!(":{}-{}", off + 1, off.saturating_add(lim))
|
||||
}
|
||||
};
|
||||
auto_read_messages.push(Item::system_message(format!(
|
||||
"[Auto-read file: {}{range}]\n{body}",
|
||||
req.path.display()
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
path = %req.path.display(),
|
||||
error = %e,
|
||||
"auto-read target could not be read; skipping",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let auto_read_messages =
|
||||
PodFsView::new(scoped_fs.clone()).render_auto_read(&final_ctx.read_required);
|
||||
|
||||
// Reference list as a single system message; omitted when empty.
|
||||
let reference_message = (!final_ctx.references.is_empty()).then(|| {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::sync::RwLock;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
use llm_worker::llm_client::types::Item;
|
||||
use protocol::Segment;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session_store::SessionId;
|
||||
|
||||
use crate::fs_view::PodFsView;
|
||||
|
||||
/// Shared state between PodController and runtime directory.
|
||||
///
|
||||
/// Controller updates this in-memory; RuntimeDir writes it to disk.
|
||||
@@ -22,6 +24,13 @@ pub struct PodSharedState {
|
||||
/// segments are not preserved). Surfaced via `Event::History` so
|
||||
/// clients can re-render typed atoms on session restore.
|
||||
pub user_segments: RwLock<Vec<Vec<Segment>>>,
|
||||
/// Pod-from-the-inside view of the filesystem. Set once in
|
||||
/// `PodController::start` after the `ScopedFs` is materialised, and
|
||||
/// read from the IPC server layer to answer `ListCompletions`
|
||||
/// queries without going through the controller. `None` until set
|
||||
/// (only relevant for unit tests that build a `PodSharedState`
|
||||
/// directly without spinning up a controller).
|
||||
fs_view: OnceLock<PodFsView>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -47,9 +56,22 @@ impl PodSharedState {
|
||||
status: RwLock::new(PodStatus::Idle),
|
||||
history: RwLock::new(Vec::new()),
|
||||
user_segments: RwLock::new(Vec::new()),
|
||||
fs_view: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach the Pod's filesystem view. Called once during controller
|
||||
/// startup. Subsequent calls are silently ignored (`OnceLock`).
|
||||
pub fn set_fs_view(&self, view: PodFsView) {
|
||||
let _ = self.fs_view.set(view);
|
||||
}
|
||||
|
||||
/// Borrow the attached `PodFsView`, if any. Returns `None` for unit
|
||||
/// tests that didn't wire one up.
|
||||
pub fn fs_view(&self) -> Option<&PodFsView> {
|
||||
self.fs_view.get()
|
||||
}
|
||||
|
||||
pub fn user_segments(&self) -> Vec<Vec<Segment>> {
|
||||
self.user_segments
|
||||
.read()
|
||||
|
||||
Reference in New Issue
Block a user