operator control: /compact slash command + endpoint
new POST /api/compact on the per-agent web UI: spawns turn::compact_session in the background so the http handler returns immediately. claude runs '/compact' over the persistent --continue session; output streams into the live panel like any other turn. slash command /compact wired to the new endpoint. SLASH_COMMANDS list now lists all four (/help /clear /cancel /compact). postCancelTurn + postCompact share a postSimple() helper. deliberately not gated against an in-flight turn — claude's own session lock will reject a concurrent compact and the failure surfaces as a Note in the live panel.
This commit is contained in:
parent
5ee65d2f15
commit
c9647f4106
2 changed files with 45 additions and 7 deletions
|
|
@ -164,20 +164,23 @@
|
||||||
{ name: '/help', desc: 'list slash commands' },
|
{ name: '/help', desc: 'list slash commands' },
|
||||||
{ name: '/clear', desc: 'wipe the terminal panel (local-only)' },
|
{ name: '/clear', desc: 'wipe the terminal panel (local-only)' },
|
||||||
{ name: '/cancel', desc: 'SIGINT the in-flight claude turn' },
|
{ name: '/cancel', desc: 'SIGINT the in-flight claude turn' },
|
||||||
|
{ name: '/compact', desc: 'compact the persistent claude session' },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function postCancelTurn() {
|
async function postSimple(url, label) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/cancel', { method: 'POST', redirect: 'manual' });
|
const resp = await fetch(url, { method: 'POST', redirect: 'manual' });
|
||||||
const ok = resp.ok || resp.type === 'opaqueredirect'
|
const ok = resp.ok || resp.type === 'opaqueredirect'
|
||||||
|| (resp.status >= 200 && resp.status < 400);
|
|| (resp.status >= 200 && resp.status < 400);
|
||||||
if (!ok && termAPI) {
|
if (!ok && termAPI) {
|
||||||
termAPI.row('turn-end-fail', '✗ /cancel failed: http ' + resp.status);
|
termAPI.row('turn-end-fail', '✗ ' + label + ' failed: http ' + resp.status);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (termAPI) termAPI.row('turn-end-fail', '✗ /cancel failed: ' + err);
|
if (termAPI) termAPI.row('turn-end-fail', '✗ ' + label + ' failed: ' + err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const postCancelTurn = () => postSimple('/api/cancel', '/cancel');
|
||||||
|
const postCompact = () => postSimple('/api/compact', '/compact');
|
||||||
|
|
||||||
function handleSlashCommand(line) {
|
function handleSlashCommand(line) {
|
||||||
if (!termAPI) return false;
|
if (!termAPI) return false;
|
||||||
|
|
@ -198,6 +201,9 @@
|
||||||
case '/cancel':
|
case '/cancel':
|
||||||
postCancelTurn();
|
postCancelTurn();
|
||||||
return true;
|
return true;
|
||||||
|
case '/compact':
|
||||||
|
postCompact();
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
termAPI.row('turn-end-fail', '✗ unknown slash command: ' + cmd + ' — try /help');
|
termAPI.row('turn-end-fail', '✗ unknown slash command: ' + cmd + ' — try /help');
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ pub async fn serve(
|
||||||
.route("/login/code", post(post_login_code))
|
.route("/login/code", post(post_login_code))
|
||||||
.route("/login/cancel", post(post_login_cancel))
|
.route("/login/cancel", post(post_login_cancel))
|
||||||
.route("/api/cancel", post(post_cancel_turn))
|
.route("/api/cancel", post(post_cancel_turn))
|
||||||
|
.route("/api/compact", post(post_compact))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
let listener = tokio::net::TcpListener::bind(addr)
|
let listener = tokio::net::TcpListener::bind(addr)
|
||||||
|
|
@ -274,6 +275,37 @@ async fn post_login_cancel(State(state): State<AppState>) -> Response {
|
||||||
Redirect::to("/").into_response()
|
Redirect::to("/").into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Operator-initiated session compaction. Spawns `turn::compact_session`
|
||||||
|
/// in the background — the HTTP handler returns immediately so the
|
||||||
|
/// async-form spinner can clear. Output (claude's compaction stream,
|
||||||
|
/// the "/compact done" note) lands in the live event panel like any
|
||||||
|
/// other turn. If a regular turn is in flight, claude's own session
|
||||||
|
/// lock will reject this one and we surface the error as a Note.
|
||||||
|
async fn post_compact(State(state): State<AppState>) -> Response {
|
||||||
|
let bus = state.bus.clone();
|
||||||
|
let socket = state.socket.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
bus.emit(crate::events::LiveEvent::Note(
|
||||||
|
"operator: /compact — running on persistent session".into(),
|
||||||
|
));
|
||||||
|
let settings = match crate::turn::write_settings(&socket).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
bus.emit(crate::events::LiveEvent::Note(format!(
|
||||||
|
"/compact failed: settings write — {e:#}"
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = crate::turn::compact_session(&settings, &bus).await {
|
||||||
|
bus.emit(crate::events::LiveEvent::Note(format!(
|
||||||
|
"/compact failed: {e:#}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
/// Cancel the in-flight claude turn. Coarse-grained: shells out
|
/// Cancel the in-flight claude turn. Coarse-grained: shells out
|
||||||
/// `pkill -INT claude` since there's at most one claude per container.
|
/// `pkill -INT claude` since there's at most one claude per container.
|
||||||
/// SIGINT (not SIGTERM) so claude flushes anything in-flight and emits a
|
/// SIGINT (not SIGTERM) so claude flushes anything in-flight and emits a
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue