Skip to content

Commit eba687f

Browse files
Add native application commands with guarded manual operations and Git review
1 parent 27c708e commit eba687f

7 files changed

Lines changed: 2066 additions & 4 deletions

File tree

‎docs/NATIVE_MIGRATION.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ Implemented foundation:
8383
- Context compaction preserves complete tool-call groups and the current request.
8484
Explicit requests to read a named file attach its current confined contents.
8585
Providers that omit token usage receive labelled estimates for budget checks.
86+
- A shared native command service for onboarding/model configuration, project
87+
trust, sessions/branching/export/pins, task replay and approvals, files,
88+
instructions/skill storage, terminal commands, Git review, and checkpoint rewind.
89+
Manual mutations reserve the same workspace registry as agent tasks. Shutdown
90+
cancels manual commands and waits for their cleanup; navigation preserves audit
91+
attribution. Per-hunk staging/discard validates the current diff, and exports
92+
traverse the full event history up to an explicit 32 MB limit.
8693
- [Native engine verification](NATIVE_VERIFICATION.md), including scripted failure
8794
cases, concurrent tasks, and real coding probes against two local models.
8895

‎docs/NATIVE_VERIFICATION.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ remain in [NATIVE_MIGRATION.md](NATIVE_MIGRATION.md).
66

77
## Automated checks
88

9-
`cargo fmt --all --check`, Clippy with warnings denied, and all **50 native
9+
`cargo fmt --all --check`, Clippy with warnings denied, and all **60 native
1010
integration tests** pass on the development machine. The suite covers:
1111

1212
- Config validation, private secrets, untrusted project overlays, profile locks,
@@ -22,6 +22,14 @@ integration tests** pass on the development machine. The suite covers:
2222
verification retries, token limits, and labelled estimates for absent usage.
2323
- Streaming UTF-8/SSE/NDJSON, malformed/truncated provider responses, large frame
2424
batches, bounded process output, concurrent commands, and process-group cleanup.
25+
- Application commands for onboarding, trusted/read-only projects, credentials,
26+
sessions, fresh-task approvals, and deletion after a project folder is removed.
27+
- Manual terminal/agent workspace exclusion, cancellation of a terminal's child
28+
process during shutdown, and attribution to its original session after navigation.
29+
- Git staging/discard of individual hunks, stale-hunk rejection, literal filenames,
30+
untracked and binary previews, and files without a final newline.
31+
- Export of 10,005 events without silently truncating history, ordered pagination,
32+
and snapshot cursors that cannot skip an event arriving during session loading.
2533

2634
The host's distro `rustdoc` needs its LLVM library directory in the loader path
2735
for doc tests. The full suite was run with:

‎native/core/src/engine.rs‎

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
1717
use serde_json::{json, Value};
1818
use std::{
1919
collections::{HashMap, VecDeque},
20-
path::PathBuf,
20+
path::{Path, PathBuf},
2121
sync::{
2222
atomic::{AtomicBool, Ordering},
2323
Arc, Mutex,
@@ -75,6 +75,33 @@ struct Running {
7575
struct QueueState {
7676
jobs: HashMap<String, Arc<Running>>,
7777
lanes: HashMap<PathBuf, VecDeque<Arc<Running>>>,
78+
manual: HashMap<PathBuf, Arc<ManualState>>,
79+
}
80+
struct ManualState {
81+
cancel: CancellationToken,
82+
finished: AtomicBool,
83+
done: Notify,
84+
}
85+
/// Holds an exclusive manual-write reservation in the same registry as agent
86+
/// jobs. Dropping it releases the workspace even if its request is cancelled.
87+
pub struct WorkspaceReservation {
88+
engine: Engine,
89+
workspace: PathBuf,
90+
state: Arc<ManualState>,
91+
}
92+
impl WorkspaceReservation {
93+
pub fn cancellation(&self) -> CancellationToken {
94+
self.state.cancel.clone()
95+
}
96+
}
97+
impl Drop for WorkspaceReservation {
98+
fn drop(&mut self) {
99+
if let Ok(mut queues) = self.engine.0.queues.lock() {
100+
queues.manual.remove(&self.workspace);
101+
}
102+
self.state.finished.store(true, Ordering::Release);
103+
self.state.done.notify_waiters();
104+
}
78105
}
79106
struct Inner {
80107
paths: AppPaths,
@@ -119,6 +146,60 @@ impl Engine {
119146
pub fn paths(&self) -> &AppPaths {
120147
&self.0.paths
121148
}
149+
pub fn delete_session(&self, id: &str) -> Result<bool> {
150+
// Starting a job uses this same lock through session lookup and insert.
151+
// No filesystem lookup is needed to delete a session for a missing folder.
152+
let queues = self
153+
.0
154+
.queues
155+
.lock()
156+
.map_err(|_| anyhow!("Task queue lock poisoned"))?;
157+
let session = self.0.store.session(id)?.context("Session not found")?;
158+
let workspace = Path::new(
159+
session["workspace"]
160+
.as_str()
161+
.context("Missing session workspace")?,
162+
);
163+
ensure!(
164+
!queues.manual.contains_key(workspace),
165+
"Wait for the manual operation to finish before deleting this session"
166+
);
167+
self.0.store.delete_session(id)
168+
}
169+
pub fn reserve_workspace(&self, workspace: &Path) -> Result<WorkspaceReservation> {
170+
let workspace = workspace.canonicalize()?;
171+
let mut queues = self
172+
.0
173+
.queues
174+
.lock()
175+
.map_err(|_| anyhow!("Task queue lock poisoned"))?;
176+
ensure!(
177+
!self.0.closing.load(Ordering::Acquire),
178+
"Application is shutting down"
179+
);
180+
ensure!(
181+
!queues.manual.contains_key(&workspace),
182+
"A manual operation is already using this workspace"
183+
);
184+
ensure!(
185+
!queues
186+
.lanes
187+
.get(&workspace)
188+
.is_some_and(|lane| lane.iter().any(|job| !job.finished.load(Ordering::Acquire))),
189+
"Stop the running task before making manual changes"
190+
);
191+
let state = Arc::new(ManualState {
192+
cancel: CancellationToken::new(),
193+
finished: AtomicBool::new(false),
194+
done: Notify::new(),
195+
});
196+
queues.manual.insert(workspace.clone(), state.clone());
197+
Ok(WorkspaceReservation {
198+
engine: self.clone(),
199+
workspace,
200+
state,
201+
})
202+
}
122203
pub async fn start(&self, request: StartRequest) -> Result<Job> {
123204
ensure!(
124205
!self.0.closing.load(Ordering::Acquire),
@@ -155,6 +236,10 @@ impl Engine {
155236
queues.jobs.len() < 64,
156237
"At most 64 tasks may be running or queued"
157238
);
239+
ensure!(
240+
!queues.manual.contains_key(&workspace.path),
241+
"Wait for the manual operation in this workspace to finish before starting a task"
242+
);
158243
let has_lane = queues.lanes.contains_key(&workspace.path);
159244
let busy = queues
160245
.lanes
@@ -306,6 +391,18 @@ impl Engine {
306391
}
307392
pub async fn shutdown(&self) -> Result<()> {
308393
self.0.closing.store(true, Ordering::Release);
394+
let manual: Vec<_> = self
395+
.0
396+
.queues
397+
.lock()
398+
.map_err(|_| anyhow!("Task queue lock poisoned"))?
399+
.manual
400+
.values()
401+
.cloned()
402+
.collect();
403+
for operation in &manual {
404+
operation.cancel.cancel();
405+
}
309406
let jobs: Vec<_> = self
310407
.0
311408
.queues
@@ -320,6 +417,15 @@ impl Engine {
320417
self.0.approvals.deny_task(&job.snapshot()?.task_id);
321418
}
322419
tokio::time::timeout(Duration::from_secs(15), async {
420+
for operation in manual {
421+
loop {
422+
let notified = operation.done.notified();
423+
if operation.finished.load(Ordering::Acquire) {
424+
break;
425+
}
426+
notified.await;
427+
}
428+
}
323429
for job in jobs {
324430
loop {
325431
let notified = job.done.notified();

‎native/core/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod patch;
1010
pub mod paths;
1111
pub mod permissions;
1212
pub mod process;
13+
pub mod service;
1314
pub mod store;
1415
pub mod tools;
1516
pub mod workspace;

0 commit comments

Comments
 (0)