workspace: remove input readiness capability

This commit is contained in:
Keisuke Hirata 2026-07-12 23:43:44 +09:00
parent c948b669c1
commit 4a90267ebc
No known key found for this signature in database
5 changed files with 31 additions and 57 deletions

View File

@ -170,7 +170,6 @@ pub struct RuntimeCapabilitySummary {
pub can_get_worker: bool, pub can_get_worker: bool,
pub can_spawn_worker: bool, pub can_spawn_worker: bool,
pub can_stop_worker: bool, pub can_stop_worker: bool,
pub can_accept_input: bool,
pub has_workspace_fs: bool, pub has_workspace_fs: bool,
pub has_shell: bool, pub has_shell: bool,
pub has_git: bool, pub has_git: bool,
@ -223,7 +222,6 @@ pub struct WorkerImplementationSummary {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerCapabilitySummary { pub struct WorkerCapabilitySummary {
pub can_accept_input: bool,
pub can_stop: bool, pub can_stop: bool,
pub can_spawn_followup: bool, pub can_spawn_followup: bool,
} }
@ -1204,14 +1202,6 @@ impl EmbeddedWorkerRuntime {
)) ))
} }
fn can_accept_embedded_input(
&self,
status: EmbeddedWorkerStatus,
execution: &worker_runtime::execution::WorkerExecutionStatus,
) -> bool {
runtime_worker_can_accept_input(self.execution_enabled, status, execution)
}
fn can_stop_embedded_worker( fn can_stop_embedded_worker(
&self, &self,
status: EmbeddedWorkerStatus, status: EmbeddedWorkerStatus,
@ -1242,8 +1232,6 @@ impl EmbeddedWorkerRuntime {
display_hint: "backend-internal worker-runtime Worker".to_string(), display_hint: "backend-internal worker-runtime Worker".to_string(),
}, },
capabilities: WorkerCapabilitySummary { capabilities: WorkerCapabilitySummary {
can_accept_input: self
.can_accept_embedded_input(summary.status, &summary.execution),
can_stop: self.can_stop_embedded_worker(summary.status, &summary.execution), can_stop: self.can_stop_embedded_worker(summary.status, &summary.execution),
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -1278,7 +1266,6 @@ impl EmbeddedWorkerRuntime {
display_hint: "backend-internal worker-runtime Worker".to_string(), display_hint: "backend-internal worker-runtime Worker".to_string(),
}, },
capabilities: WorkerCapabilitySummary { capabilities: WorkerCapabilitySummary {
can_accept_input: self.can_accept_embedded_input(detail.status, &detail.execution),
can_stop: self.can_stop_embedded_worker(detail.status, &detail.execution), can_stop: self.can_stop_embedded_worker(detail.status, &detail.execution),
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -2014,7 +2001,6 @@ impl RemoteWorkerRuntime {
display_hint: "Backend-proxied remote worker-runtime Worker".to_string(), display_hint: "Backend-proxied remote worker-runtime Worker".to_string(),
}, },
capabilities: WorkerCapabilitySummary { capabilities: WorkerCapabilitySummary {
can_accept_input: runtime_worker_can_accept_input(true, summary.status, &summary.execution),
can_stop: runtime_worker_can_stop(true, summary.status, &summary.execution), can_stop: runtime_worker_can_stop(true, summary.status, &summary.execution),
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -2049,7 +2035,6 @@ impl RemoteWorkerRuntime {
display_hint: "Backend-proxied remote worker-runtime Worker".to_string(), display_hint: "Backend-proxied remote worker-runtime Worker".to_string(),
}, },
capabilities: WorkerCapabilitySummary { capabilities: WorkerCapabilitySummary {
can_accept_input: runtime_worker_can_accept_input(true, detail.status, &detail.execution),
can_stop: runtime_worker_can_stop(true, detail.status, &detail.execution), can_stop: runtime_worker_can_stop(true, detail.status, &detail.execution),
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -2459,7 +2444,6 @@ fn embedded_runtime_capabilities(
can_get_worker: available, can_get_worker: available,
can_spawn_worker: available, can_spawn_worker: available,
can_stop_worker: available && execution_enabled, can_stop_worker: available && execution_enabled,
can_accept_input: available && execution_enabled,
has_workspace_fs: false, has_workspace_fs: false,
has_shell: false, has_shell: false,
has_git: false, has_git: false,
@ -2508,18 +2492,6 @@ fn embedded_spawn_execution_failure_diagnostic(
)) ))
} }
fn runtime_worker_can_accept_input(
execution_enabled: bool,
status: EmbeddedWorkerStatus,
execution: &WorkerExecutionStatus,
) -> bool {
execution_enabled
&& status == EmbeddedWorkerStatus::Running
&& execution.backend == worker_runtime::execution::WorkerExecutionBackendKind::Connected
&& execution.run_state == WorkerExecutionRunState::Idle
&& !execution_last_result_blocks_control(execution)
}
fn runtime_worker_can_stop( fn runtime_worker_can_stop(
execution_enabled: bool, execution_enabled: bool,
status: EmbeddedWorkerStatus, status: EmbeddedWorkerStatus,
@ -2980,7 +2952,6 @@ fn remote_runtime_capabilities(
can_get_worker: available, can_get_worker: available,
can_spawn_worker: available && worker_creation_available, can_spawn_worker: available && worker_creation_available,
can_stop_worker: available, can_stop_worker: available,
can_accept_input: available && worker_creation_available,
has_workspace_fs: false, has_workspace_fs: false,
has_shell: false, has_shell: false,
has_git: false, has_git: false,
@ -3208,7 +3179,6 @@ pub fn placeholder_worker(host_id: impl Into<String>) -> WorkerSummary {
display_hint: "unsupported".to_string(), display_hint: "unsupported".to_string(),
}, },
capabilities: WorkerCapabilitySummary { capabilities: WorkerCapabilitySummary {
can_accept_input: false,
can_stop: false, can_stop: false,
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -3535,7 +3505,6 @@ mod tests {
display_hint: "test fixture".to_string(), display_hint: "test fixture".to_string(),
}, },
capabilities: WorkerCapabilitySummary { capabilities: WorkerCapabilitySummary {
can_accept_input: false,
can_stop: false, can_stop: false,
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -3565,7 +3534,6 @@ mod tests {
can_get_worker: true, can_get_worker: true,
can_spawn_worker: false, can_spawn_worker: false,
can_stop_worker: false, can_stop_worker: false,
can_accept_input: false,
has_workspace_fs: false, has_workspace_fs: false,
has_shell: false, has_shell: false,
has_git: false, has_git: false,
@ -3749,7 +3717,6 @@ mod tests {
let spawned = runtime.spawn_worker(embedded_spawn_request()); let spawned = runtime.spawn_worker(embedded_spawn_request());
assert_eq!(spawned.state, WorkerOperationState::Accepted); assert_eq!(spawned.state, WorkerOperationState::Accepted);
let worker = spawned.worker.expect("created embedded worker"); let worker = spawned.worker.expect("created embedded worker");
assert!(worker.capabilities.can_accept_input);
assert!(worker.capabilities.can_stop); assert!(worker.capabilities.can_stop);
let input = runtime.send_input( let input = runtime.send_input(
@ -3768,7 +3735,6 @@ mod tests {
.worker .worker
.expect("worker detail"); .expect("worker detail");
if detail.state == "idle" { if detail.state == "idle" {
assert!(detail.capabilities.can_accept_input);
break; break;
} }
assert!( assert!(
@ -3801,7 +3767,6 @@ mod tests {
); );
assert_eq!(embedded_summary.source.status, RuntimeSourceStatus::Active); assert_eq!(embedded_summary.source.status, RuntimeSourceStatus::Active);
assert!(embedded_summary.capabilities.can_spawn_worker); assert!(embedded_summary.capabilities.can_spawn_worker);
assert!(embedded_summary.capabilities.can_accept_input);
let spawned = registry let spawned = registry
.spawn_worker( .spawn_worker(
@ -3837,8 +3802,6 @@ mod tests {
assert_eq!(worker.workspace.identity, "runtime_registry_worker"); assert_eq!(worker.workspace.identity, "runtime_registry_worker");
assert_eq!(worker.implementation.kind, "embedded_worker_runtime"); assert_eq!(worker.implementation.kind, "embedded_worker_runtime");
assert_eq!(worker.profile.as_deref(), Some("builtin:coder")); assert_eq!(worker.profile.as_deref(), Some("builtin:coder"));
assert!(worker.capabilities.can_accept_input);
let input = registry let input = registry
.send_input( .send_input(
EMBEDDED_RUNTIME_ID, EMBEDDED_RUNTIME_ID,
@ -4052,7 +4015,6 @@ mod tests {
workers.items[0].workspace.identity, workers.items[0].workspace.identity,
"runtime_registry_worker" "runtime_registry_worker"
); );
assert!(workers.items[0].capabilities.can_accept_input);
assert!(workers.items[0].capabilities.can_stop); assert!(workers.items[0].capabilities.can_stop);
let input = registry let input = registry
@ -4157,11 +4119,6 @@ mod tests {
let workers = registry.list_workers(10); let workers = registry.list_workers(10);
assert_eq!(workers.items.len(), 4); assert_eq!(workers.items.len(), 4);
for worker in &workers.items { for worker in &workers.items {
assert!(
!worker.capabilities.can_accept_input,
"{} should not be input-capable",
worker.worker_id
);
assert!( assert!(
!worker.capabilities.can_stop, !worker.capabilities.can_stop,
"{} should not be stoppable", "{} should not be stoppable",
@ -4174,7 +4131,6 @@ mod tests {
assert_eq!(workers.items[3].state, "errored"); assert_eq!(workers.items[3].state, "errored");
let stale_detail = registry.worker("remote:primary", "1").unwrap(); let stale_detail = registry.worker("remote:primary", "1").unwrap();
assert!(!stale_detail.capabilities.can_accept_input);
assert!(!stale_detail.capabilities.can_stop); assert!(!stale_detail.capabilities.can_stop);
assert_eq!(stale_detail.state, "stale"); assert_eq!(stale_detail.state, "stale");

View File

@ -4098,7 +4098,6 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary
pinned: record.retention_state == "pinned", pinned: record.retention_state == "pinned",
retention_state: record.retention_state.clone(), retention_state: record.retention_state.clone(),
capabilities: WorkerCapabilitySummary { capabilities: WorkerCapabilitySummary {
can_accept_input: false,
can_stop: false, can_stop: false,
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -7142,7 +7141,6 @@ mod tests {
.worker("embedded-worker-runtime", &worker_id) .worker("embedded-worker-runtime", &worker_id)
.expect("restored worker"); .expect("restored worker");
assert_eq!(restored_worker.state, "stale"); assert_eq!(restored_worker.state, "stale");
assert!(!restored_worker.capabilities.can_accept_input);
assert!(!restored_worker.capabilities.can_stop); assert!(!restored_worker.capabilities.can_stop);
assert!( assert!(
restored_worker restored_worker

View File

@ -35,7 +35,6 @@ export type RuntimeCapabilities = {
can_get_worker: boolean; can_get_worker: boolean;
can_spawn_worker: boolean; can_spawn_worker: boolean;
can_stop_worker: boolean; can_stop_worker: boolean;
can_accept_input: boolean;
has_workspace_fs: boolean; has_workspace_fs: boolean;
has_shell: boolean; has_shell: boolean;
has_git: boolean; has_git: boolean;
@ -70,7 +69,6 @@ export type Host = {
}; };
export type WorkerCapabilities = { export type WorkerCapabilities = {
can_accept_input: boolean;
can_stop: boolean; can_stop: boolean;
can_spawn_followup: boolean; can_spawn_followup: boolean;
}; };

View File

@ -29,7 +29,6 @@ function worker(overrides: Partial<Worker>): Worker {
display_hint: "Runtime Worker", display_hint: "Runtime Worker",
}, },
capabilities: { capabilities: {
can_accept_input: true,
can_stop: true, can_stop: true,
can_spawn_followup: false, can_spawn_followup: false,
}, },
@ -47,7 +46,6 @@ Deno.test("registry-only workers are not sidebar targets or console targets", ()
display_hint: "Missing Worker", display_hint: "Missing Worker",
}, },
capabilities: { capabilities: {
can_accept_input: false,
can_stop: false, can_stop: false,
can_spawn_followup: false, can_spawn_followup: false,
}, },

View File

@ -10,7 +10,8 @@
ClientWorkerEventWsFrame, ClientWorkerEventWsFrame,
Diagnostic, Diagnostic,
Worker, Worker,
WorkerInputResult WorkerInputResult,
PodProtocolEvent
} from '$lib/workspace-sidebar/types'; } from '$lib/workspace-sidebar/types';
type Props = { type Props = {
@ -32,6 +33,7 @@
} }
let worker = $state<Worker | null>(null); let worker = $state<Worker | null>(null);
let liveWorkerState = $state<string | null>(null);
let workerError = $state<string | null>(null); let workerError = $state<string | null>(null);
let draft = $state(''); let draft = $state('');
let sending = $state(false); let sending = $state(false);
@ -59,7 +61,9 @@
); );
const lines = $derived(projection.lines); const lines = $derived(projection.lines);
const diagnostics = $derived(mergeDiagnostics(worker?.diagnostics ?? [], streamDiagnostics)); const diagnostics = $derived(mergeDiagnostics(worker?.diagnostics ?? [], streamDiagnostics));
const canSend = $derived(Boolean(worker?.capabilities.can_accept_input) && draft.trim().length > 0 && !sending); const workerState = $derived(liveWorkerState ?? worker?.state ?? 'loading');
const inputReady = $derived(workerState === 'idle');
const canSend = $derived(inputReady && draft.trim().length > 0 && !sending);
async function getJson<T>(path: string): Promise<T> { async function getJson<T>(path: string): Promise<T> {
const response = await fetch(path); const response = await fetch(path);
@ -97,12 +101,15 @@
async function loadWorker(target: ConsoleTarget) { async function loadWorker(target: ConsoleTarget) {
workerError = null; workerError = null;
try { try {
worker = await getJson<Worker>( const payload = await getJson<Worker>(
workerApiPath(`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}`) workerApiPath(`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}`)
); );
worker = payload;
liveWorkerState = payload.state;
} catch (error) { } catch (error) {
workerError = error instanceof Error ? error.message : String(error); workerError = error instanceof Error ? error.message : String(error);
worker = null; worker = null;
liveWorkerState = null;
} }
} }
@ -132,7 +139,7 @@
async function sendMessage(event: SubmitEvent) { async function sendMessage(event: SubmitEvent) {
event.preventDefault(); event.preventDefault();
const content = draft.trim(); const content = draft.trim();
if (!content || sending || !worker?.capabilities.can_accept_input) { if (!content || sending || !inputReady) {
return; return;
} }
@ -145,6 +152,7 @@
); );
if (result.state === 'accepted') { if (result.state === 'accepted') {
draft = ''; draft = '';
liveWorkerState = 'running';
} else { } else {
sendError = diagnosticsToText(result.diagnostics) || `Input was ${result.state}.`; sendError = diagnosticsToText(result.diagnostics) || `Input was ${result.state}.`;
} }
@ -155,6 +163,18 @@
} }
} }
function workerStateFromProtocolEvent(event: PodProtocolEvent): string | null {
switch (event.event) {
case 'snapshot':
case 'status':
return event.data.status;
case 'shutdown':
return 'shutdown';
default:
return null;
}
}
function connectObservation(targetWorker: Worker | null, token: number, target: ConsoleTarget) { function connectObservation(targetWorker: Worker | null, token: number, target: ConsoleTarget) {
if (!targetWorker) { if (!targetWorker) {
streamState = 'closed'; streamState = 'closed';
@ -182,6 +202,10 @@
if (!rememberObservationEvent(frame.envelope.event_id)) { if (!rememberObservationEvent(frame.envelope.event_id)) {
return; return;
} }
const observedState = workerStateFromProtocolEvent(frame.envelope.payload);
if (observedState) {
liveWorkerState = observedState;
}
observedEvents = [ observedEvents = [
...observedEvents, ...observedEvents,
{ {
@ -304,6 +328,7 @@
$effect(() => { $effect(() => {
const target = consoleTarget; const target = consoleTarget;
resetObservedEvents(); resetObservedEvents();
liveWorkerState = null;
streamDiagnostics = []; streamDiagnostics = [];
advanceReloadToken(); advanceReloadToken();
void loadConsoleData(target); void loadConsoleData(target);
@ -324,7 +349,7 @@
</div> </div>
<div class="console-header-actions"> <div class="console-header-actions">
<div class="console-status-pill" class:warn={streamState !== 'open'}> <div class="console-status-pill" class:warn={streamState !== 'open'}>
{worker?.state ?? 'loading'} · stream {streamState} {workerState} · stream {streamState}
</div> </div>
<button type="button" class="secondary-button" aria-expanded={workerDetailsOpen} onclick={() => workerDetailsOpen = !workerDetailsOpen}> <button type="button" class="secondary-button" aria-expanded={workerDetailsOpen} onclick={() => workerDetailsOpen = !workerDetailsOpen}>
Details Details
@ -430,7 +455,6 @@
<details class="metadata-details"> <details class="metadata-details">
<summary>Capabilities</summary> <summary>Capabilities</summary>
<ul> <ul>
<li>input: {worker.capabilities.can_accept_input ? 'available' : 'unsupported'}</li>
<li>stop: {worker.capabilities.can_stop ? 'available' : 'unsupported'}</li> <li>stop: {worker.capabilities.can_stop ? 'available' : 'unsupported'}</li>
<li>follow-up spawn: {worker.capabilities.can_spawn_followup ? 'available' : 'unsupported'}</li> <li>follow-up spawn: {worker.capabilities.can_spawn_followup ? 'available' : 'unsupported'}</li>
</ul> </ul>
@ -461,7 +485,7 @@
id="worker-console-message" id="worker-console-message"
aria-label="Console input" aria-label="Console input"
bind:value={draft} bind:value={draft}
disabled={!worker?.capabilities.can_accept_input || sending} disabled={!inputReady || sending}
></textarea> ></textarea>
<div class="composer-actions"> <div class="composer-actions">
<button type="submit" disabled={!canSend}>{sending ? 'Sending…' : 'Send'}</button> <button type="submit" disabled={!canSend}>{sending ? 'Sending…' : 'Send'}</button>