chore: merge current develop into T-563 source

This commit is contained in:
2026-09-01 02:39:08 +09:00
21 changed files with 1763 additions and 281 deletions
+6 -1
View File
@@ -38,5 +38,10 @@ pub use target::{
WorkerConnection, WorkerConnectionSelector, WorkerList, WorkerListRequest, WorkerSpawn,
};
pub use worker_client::WorkerClient;
pub use workspace_api::{ObjectiveDetail, ObjectiveSummary};
pub use workspace_api::{
CompanionCancelRequest, CompanionLifecycleState, CompanionMessageDisposition,
CompanionMessageRequest, CompanionMessageResponse, CompanionStatusResponse,
CompanionTranscriptItem, CompanionTranscriptProjection, CompanionTranscriptRole,
CompanionTransportSummary, ObjectiveDetail, ObjectiveSummary,
};
pub use workspace_product::BackendWorkspaceProductClient;
+9
View File
@@ -12,6 +12,7 @@ typescript = ["dep:ts-rs"]
[dependencies]
serde = { workspace = true, features = ["derive"] }
ts-rs = { version = "12.0.1", optional = true }
url.workspace = true
[dev-dependencies]
serde_json.workspace = true
@@ -19,3 +20,11 @@ serde_json.workspace = true
[[example]]
name = "generate_workdir_api_types"
required-features = ["typescript"]
[[example]]
name = "generate_companion_api_types"
required-features = ["typescript"]
[[example]]
name = "generate_repository_access_types"
required-features = ["typescript"]
@@ -0,0 +1,3 @@
fn main() {
print!("{}", workspace_api::companion_api_typescript());
}
@@ -0,0 +1,3 @@
fn main() {
print!("{}", workspace_api::repository_access_api_typescript());
}
+381 -1
View File
@@ -465,6 +465,7 @@ pub struct WorkerCapabilitySummary {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WorkspaceWorkerSubject {
RuntimeWorker {
@@ -477,6 +478,7 @@ pub enum WorkspaceWorkerSubject {
/// Runtime placement appears only in the typed subject required by Worker
/// control operations; provider and launch internals are intentionally omitted.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkspaceWorkerDiscoveryItem {
pub subject: WorkspaceWorkerSubject,
pub resource_key: String,
@@ -486,6 +488,137 @@ pub struct WorkspaceWorkerDiscoveryItem {
pub status: Option<String>,
}
/// Public lifecycle projection for the Workspace Companion endpoint.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum CompanionLifecycleState {
Idle,
Running,
Stopped,
}
/// Public outcome of a Companion message submission.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum CompanionMessageDisposition {
Accepted,
Rejected,
}
/// Public, bounded transport metadata for Companion status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CompanionTransportSummary {
pub mode: String,
pub available: bool,
}
/// Public Workspace Companion status response.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CompanionStatusResponse {
pub state: CompanionLifecycleState,
pub worker: Option<WorkspaceWorkerDiscoveryItem>,
pub transport: CompanionTransportSummary,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
/// Public Workspace Companion message request.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CompanionMessageRequest {
pub content: String,
}
/// Public Workspace Companion cancellation request.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CompanionCancelRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
/// Public Workspace Companion message response.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CompanionMessageResponse {
pub state: CompanionMessageDisposition,
pub message: String,
}
/// User-visible role accepted in the public Companion transcript.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum CompanionTranscriptRole {
User,
Assistant,
}
/// One allowlisted, user-visible Companion transcript item.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CompanionTranscriptItem {
pub sequence: usize,
pub role: CompanionTranscriptRole,
pub content: String,
pub created_at: String,
}
/// Bounded public Companion transcript projection.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CompanionTranscriptProjection {
pub state: CompanionLifecycleState,
pub start: usize,
pub limit: usize,
pub total: usize,
pub next: Option<usize>,
pub items: Vec<CompanionTranscriptItem>,
}
#[cfg(feature = "typescript")]
pub fn companion_api_typescript() -> String {
use ts_rs::TS;
let config = ts_rs::Config::default();
let declarations = [
DiagnosticSeverity::decl(&config),
Diagnostic::decl(&config),
WorkspaceWorkerSubject::decl(&config),
WorkspaceWorkerDiscoveryItem::decl(&config),
CompanionLifecycleState::decl(&config),
CompanionMessageDisposition::decl(&config),
CompanionTransportSummary::decl(&config),
CompanionStatusResponse::decl(&config),
CompanionMessageRequest::decl(&config),
CompanionCancelRequest::decl(&config),
CompanionMessageResponse::decl(&config),
CompanionTranscriptRole::decl(&config),
CompanionTranscriptItem::decl(&config),
CompanionTranscriptProjection::decl(&config),
];
format!(
"// Generated by `cargo run -p workspace-api --features typescript --example generate_companion_api_types`.\n// Do not edit manually.\n\n{}\n",
declarations
.into_iter()
.map(|declaration| format!("export {declaration}"))
.collect::<Vec<_>>()
.join("\n\n")
)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkspaceWorkerDiscoveryPage {
pub workers: Vec<WorkspaceWorkerDiscoveryItem>,
@@ -571,6 +704,7 @@ pub struct UpdateWorkspaceMemorySettingsRequest {
///
/// Secret references and secret material are deliberately not part of this DTO.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RepositorySshCredential {
pub credential_id: String,
@@ -578,6 +712,7 @@ pub struct RepositorySshCredential {
pub name: String,
pub public_key_algorithm: String,
pub public_key_fingerprint: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub current_revision: u64,
pub status: String,
pub created_at: String,
@@ -587,6 +722,7 @@ pub struct RepositorySshCredential {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct CreateRepositorySshCredentialRequest {
pub operation_id: String,
@@ -598,9 +734,11 @@ pub struct CreateRepositorySshCredentialRequest {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RotateRepositorySshCredentialRequest {
pub operation_id: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub expected_revision: u64,
pub private_key: String,
#[serde(default)]
@@ -608,14 +746,17 @@ pub struct RotateRepositorySshCredentialRequest {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DeleteRepositorySshCredentialRequest {
pub operation_id: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub expected_revision: u64,
}
/// Public metadata for an explicitly pinned SSH host key.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RepositorySshHostTrust {
pub host_trust_id: String,
@@ -625,6 +766,7 @@ pub struct RepositorySshHostTrust {
pub key_algorithm: String,
pub host_key: String,
pub fingerprint: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub current_revision: u64,
pub created_at: String,
pub updated_at: String,
@@ -633,6 +775,7 @@ pub struct RepositorySshHostTrust {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PutRepositorySshHostTrustRequest {
pub operation_id: String,
@@ -641,17 +784,22 @@ pub struct PutRepositorySshHostTrustRequest {
pub port: u16,
pub host_key: String,
#[serde(default)]
#[cfg_attr(feature = "typescript", ts(type = "number | null"))]
pub expected_revision: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DeleteRepositorySshHostTrustRequest {
pub operation_id: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub expected_revision: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(rename_all = "snake_case"))]
#[serde(rename_all = "snake_case")]
pub enum RepositoryAccessMode {
ReadOnly,
@@ -659,6 +807,7 @@ pub enum RepositoryAccessMode {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RepositorySshAccessBinding {
pub repository_id: String,
@@ -669,14 +818,43 @@ pub struct RepositorySshAccessBinding {
/// Secret-free active Repository access projection consumed by later Runtime work.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RepositoryAccessProjection {
pub workspace_id: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub config_revision: u64,
pub projection_digest: String,
pub bindings: Vec<RepositorySshAccessBinding>,
}
#[cfg(feature = "typescript")]
pub fn repository_access_api_typescript() -> String {
use ts_rs::TS;
let config = ts_rs::Config::default();
let declarations = [
RepositorySshCredential::decl(&config),
CreateRepositorySshCredentialRequest::decl(&config),
RotateRepositorySshCredentialRequest::decl(&config),
DeleteRepositorySshCredentialRequest::decl(&config),
RepositorySshHostTrust::decl(&config),
PutRepositorySshHostTrustRequest::decl(&config),
DeleteRepositorySshHostTrustRequest::decl(&config),
RepositoryAccessMode::decl(&config),
RepositorySshAccessBinding::decl(&config),
RepositoryAccessProjection::decl(&config),
];
format!(
"// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts\n\n{}\n",
declarations
.into_iter()
.map(|declaration| format!("export {declaration}"))
.collect::<Vec<_>>()
.join("\n\n")
)
}
#[cfg(feature = "typescript")]
pub fn workdir_api_typescript() -> String {
use ts_rs::TS;
@@ -706,7 +884,7 @@ pub fn workdir_api_typescript() -> String {
}
#[cfg(all(test, feature = "typescript"))]
mod typescript_tests {
mod workdir_typescript_tests {
#[test]
fn generated_workdir_api_contract_is_current() {
let expected = super::workdir_api_typescript();
@@ -756,6 +934,161 @@ mod tests {
assert!(serde_json::from_value::<WorkerSummary>(payload).is_err());
}
fn round_trip<T>(value: T)
where
T: std::fmt::Debug + PartialEq + Serialize + for<'de> Deserialize<'de>,
{
let encoded = serde_json::to_vec(&value).expect("fixture should serialize");
let decoded: T = serde_json::from_slice(&encoded).expect("fixture should deserialize");
assert_eq!(decoded, value);
}
fn companion_worker() -> WorkspaceWorkerDiscoveryItem {
WorkspaceWorkerDiscoveryItem {
subject: WorkspaceWorkerSubject::RuntimeWorker {
runtime_id: "arcadia".to_string(),
worker_id: "worker-7".to_string(),
},
resource_key: "W-7".to_string(),
display_name: "Companion".to_string(),
profile: Some("builtin:companion".to_string()),
status: Some("idle".to_string()),
}
}
#[test]
fn companion_status_fixtures_round_trip() {
for state in [
CompanionLifecycleState::Idle,
CompanionLifecycleState::Running,
CompanionLifecycleState::Stopped,
] {
round_trip(CompanionStatusResponse {
state,
worker: Some(companion_worker()),
transport: CompanionTransportSummary {
mode: "worker_runtime".to_string(),
available: state != CompanionLifecycleState::Stopped,
},
diagnostics: Vec::new(),
});
}
}
#[test]
fn companion_message_fixtures_round_trip() {
for state in [
CompanionMessageDisposition::Accepted,
CompanionMessageDisposition::Rejected,
] {
round_trip(CompanionMessageResponse {
state,
message: if state == CompanionMessageDisposition::Accepted {
"accepted"
} else {
"rejected"
}
.to_string(),
});
}
}
#[test]
fn companion_transcript_fixture_round_trips() {
round_trip(CompanionTranscriptProjection {
state: CompanionLifecycleState::Idle,
start: 0,
limit: 2,
total: 2,
next: None,
items: vec![
CompanionTranscriptItem {
sequence: 1,
role: CompanionTranscriptRole::User,
content: "hello".to_string(),
created_at: "2026-08-31T00:00:00Z".to_string(),
},
CompanionTranscriptItem {
sequence: 2,
role: CompanionTranscriptRole::Assistant,
content: "hi".to_string(),
created_at: "2026-08-31T00:00:01Z".to_string(),
},
],
});
}
#[test]
fn companion_transcript_rejects_system_and_private_fields() {
let public_item = CompanionTranscriptItem {
sequence: 1,
role: CompanionTranscriptRole::Assistant,
content: "visible".to_string(),
created_at: "2026-08-31T00:00:00Z".to_string(),
};
let public_fields = serde_json::to_value(public_item)
.expect("public transcript item should serialize")
.as_object()
.expect("public transcript item should be an object")
.keys()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
public_fields,
["content", "created_at", "role", "sequence"]
.into_iter()
.map(str::to_string)
.collect()
);
let system_item = serde_json::json!({
"sequence": 1,
"role": "system",
"content": "raw system prompt",
"created_at": "2026-08-31T00:00:00Z"
});
assert!(serde_json::from_value::<CompanionTranscriptItem>(system_item).is_err());
let private_item = serde_json::json!({
"sequence": 1,
"role": "assistant",
"content": "visible",
"created_at": "2026-08-31T00:00:00Z",
"reasoning": "hidden",
"credential": "secret",
"provider_session_id": "session-private"
});
assert!(serde_json::from_value::<CompanionTranscriptItem>(private_item).is_err());
}
#[cfg(feature = "typescript")]
#[test]
fn generated_companion_api_contract_is_current() {
let expected = companion_api_typescript();
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../web/workspace/src/lib/generated/companion-api.ts");
let actual = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
assert_eq!(
normalize_typescript(&actual),
normalize_typescript(&expected),
"regenerate Companion API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_companion_api_types > web/workspace/src/lib/generated/companion-api.ts` and format the generated file",
);
}
#[cfg(feature = "typescript")]
fn normalize_typescript(value: &str) -> String {
value
.chars()
.filter_map(|character| match character {
character if character.is_whitespace() => None,
',' => Some(';'),
character => Some(character),
})
.collect::<String>()
.replace(";}", "}")
}
#[test]
fn workdir_create_request_preserves_optional_operation_fields() {
let payload = serde_json::json!({"repository_id": "main"});
@@ -849,3 +1182,50 @@ mod tests {
assert!(serde_json::from_value::<WorkingDirectoryListResponse>(stale).is_err());
}
}
#[cfg(all(test, feature = "typescript"))]
mod typescript_tests {
#[test]
fn generated_repository_access_contract_is_current() {
let expected = super::repository_access_api_typescript();
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../web/workspace/src/lib/generated/repository-access-api.ts");
let actual = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
assert_eq!(
normalize(&actual),
normalize(&expected),
"regenerate Repository Access TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts` and format the generated file",
);
}
#[test]
fn generated_repository_access_responses_remain_secret_free() {
use ts_rs::TS;
let config = ts_rs::Config::default();
for declaration in [
super::RepositorySshCredential::decl(&config),
super::RepositorySshHostTrust::decl(&config),
super::RepositoryAccessProjection::decl(&config),
] {
for forbidden in ["private_key", "passphrase", "secret_ref"] {
assert!(
!declaration.contains(forbidden),
"Repository Access response declaration must not expose `{forbidden}`"
);
}
}
}
fn normalize(value: &str) -> String {
value
.chars()
.filter_map(|character| match character {
character if character.is_whitespace() => None,
',' => Some(';'),
character => Some(character),
})
.collect()
}
}
+32 -113
View File
@@ -1,77 +1,14 @@
use serde::{Deserialize, Serialize};
use workspace_api::{
CompanionLifecycleState, CompanionMessageDisposition, CompanionTransportSummary, Diagnostic,
DiagnosticSeverity,
};
use crate::hosts::{DiagnosticSeverity, RuntimeDiagnostic, WorkerSummary};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CompanionState {
Disabled,
Rejected,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompanionStatusResponse {
pub state: CompanionState,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker: Option<WorkerSummary>,
pub transport: CompanionTransportSummary,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompanionTransportSummary {
pub kind: String,
pub completion: String,
pub limitation: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct CompanionMessageRequest {
pub content: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
pub struct CompanionCancelRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompanionMessageResponse {
pub state: CompanionState,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker: Option<WorkerSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_item: Option<CompanionTranscriptItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assistant_item: Option<CompanionTranscriptItem>,
pub transcript: CompanionTranscriptProjection,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompanionTranscriptProjection {
pub state: CompanionState,
pub start: usize,
pub limit: usize,
pub total_items: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_start: Option<usize>,
pub items: Vec<CompanionTranscriptItem>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompanionTranscriptItem {
pub sequence: u64,
pub role: String,
pub content: String,
pub created_at: String,
pub source: String,
pub status: String,
}
pub use workspace_api::{
CompanionCancelRequest, CompanionMessageRequest, CompanionMessageResponse,
CompanionStatusResponse, CompanionTranscriptProjection,
};
#[derive(Clone, Default)]
pub struct CompanionConsole;
impl CompanionConsole {
@@ -81,68 +18,50 @@ impl CompanionConsole {
pub fn status(&self) -> CompanionStatusResponse {
CompanionStatusResponse {
state: CompanionState::Disabled,
state: CompanionLifecycleState::Stopped,
worker: None,
transport: disabled_transport(),
transport: CompanionTransportSummary {
mode: "disabled".to_string(),
available: false,
},
diagnostics: vec![disabled_diagnostic()],
}
}
pub fn transcript(&self, start: usize, limit: usize) -> CompanionTranscriptProjection {
CompanionTranscriptProjection {
state: CompanionState::Disabled,
state: CompanionLifecycleState::Stopped,
start,
limit,
total_items: 0,
next_start: None,
total: 0,
next: None,
items: Vec::new(),
diagnostics: vec![disabled_diagnostic()],
}
}
pub fn send_message(&self, _request: CompanionMessageRequest) -> CompanionMessageResponse {
disabled_message_response(CompanionState::Rejected)
disabled_message_response()
}
pub fn cancel(&self, _request: CompanionCancelRequest) -> CompanionMessageResponse {
disabled_message_response(CompanionState::Cancelled)
disabled_message_response()
}
}
fn disabled_message_response(state: CompanionState) -> CompanionMessageResponse {
fn disabled_message_response() -> CompanionMessageResponse {
CompanionMessageResponse {
state,
worker: None,
user_item: None,
assistant_item: None,
transcript: CompanionTranscriptProjection {
state: CompanionState::Disabled,
start: 0,
limit: 200,
total_items: 0,
next_start: None,
items: Vec::new(),
diagnostics: vec![disabled_diagnostic()],
},
diagnostics: vec![disabled_diagnostic()],
}
}
fn disabled_transport() -> CompanionTransportSummary {
CompanionTransportSummary {
kind: "none".to_string(),
completion: "disabled".to_string(),
limitation:
"Workspace Companion auto-start has been removed; create an explicit Worker instead."
.to_string(),
}
}
fn disabled_diagnostic() -> RuntimeDiagnostic {
RuntimeDiagnostic {
code: "companion_disabled".to_string(),
severity: DiagnosticSeverity::Info,
message: "Workspace Companion auto-start is disabled; create an explicit Worker instead."
state: CompanionMessageDisposition::Rejected,
message: "Workspace Companion auto-start is disabled; create or select an explicit Worker instead."
.to_string(),
}
}
fn disabled_diagnostic() -> Diagnostic {
Diagnostic {
code: "companion_disabled".to_string(),
severity: DiagnosticSeverity::Info,
message:
"Workspace Companion auto-start was removed; use the explicit Worker lifecycle instead."
.to_string(),
}
}
+65 -43
View File
@@ -11085,33 +11085,43 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
}
fn companion_console_extension_point(status: &CompanionStatusResponse) -> ExtensionPointState {
let completion = status.transport.completion.clone();
let note = match completion.as_str() {
"connected" => "Workspace Companion is input-capable and browser input is dispatched through the normal Worker runtime path.".to_string(),
"not_input_capable" => {
let diagnostic_codes = status
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>()
.join(", ");
if diagnostic_codes.is_empty() {
"Workspace Companion is not input-capable; check provider, config, profile, secret, and authority diagnostics.".to_string()
} else {
format!(
"Workspace Companion is not input-capable; check typed diagnostics: {diagnostic_codes}."
)
}
}
"disabled" => "Workspace Companion auto-start has been removed; create an explicit Worker instead.".to_string(),
other => format!(
"Workspace Companion transport reports {other}; browser input follows the Companion Worker runtime capability state."
),
let extension_status = match status.state {
workspace_api::CompanionLifecycleState::Idle => "idle",
workspace_api::CompanionLifecycleState::Running => "running",
workspace_api::CompanionLifecycleState::Stopped => "stopped",
}
.to_string();
let diagnostic_codes = status
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>()
.join(", ");
let note = if status.transport.available {
"Workspace Companion is input-capable and browser input is dispatched through the normal Worker runtime path."
.to_string()
} else if diagnostic_codes.is_empty() {
"Workspace Companion is unavailable; create or select an explicit Worker instead."
.to_string()
} else {
format!("Workspace Companion is unavailable; check typed diagnostics: {diagnostic_codes}.")
};
ExtensionPointState {
status: completion,
status: extension_status,
note,
diagnostics: status.diagnostics.clone(),
diagnostics: status
.diagnostics
.iter()
.map(|diagnostic| RuntimeDiagnostic {
code: diagnostic.code.clone(),
severity: match diagnostic.severity {
workspace_api::DiagnosticSeverity::Info => DiagnosticSeverity::Info,
workspace_api::DiagnosticSeverity::Warning => DiagnosticSeverity::Warning,
workspace_api::DiagnosticSeverity::Error => DiagnosticSeverity::Error,
},
message: diagnostic.message.clone(),
})
.collect(),
}
}
@@ -24209,10 +24219,10 @@ mod tests {
);
let companion_status = get_json(app.clone(), "/api/companion/status").await;
assert_eq!(companion_status["state"], "disabled");
assert_eq!(companion_status["state"], "stopped");
assert!(companion_status["worker"].is_null());
assert_eq!(companion_status["transport"]["kind"], "none");
assert_eq!(companion_status["transport"]["completion"], "disabled");
assert_eq!(companion_status["transport"]["mode"], "disabled");
assert_eq!(companion_status["transport"]["available"], false);
assert!(!companion_status.to_string().contains("/workspace/demo"));
let companion_message = post_json(
@@ -24222,16 +24232,26 @@ mod tests {
)
.await;
assert_eq!(companion_message["state"], "rejected");
assert_eq!(
companion_message["diagnostics"][0]["code"],
"companion_disabled"
);
assert!(companion_message["user_item"].is_null());
assert!(companion_message["assistant_item"].is_null());
assert!(companion_message.get("accepted").is_none());
assert!(companion_message.get("diagnostics").is_none());
assert!(companion_message.get("user_item").is_none());
assert!(companion_message.get("assistant_item").is_none());
assert!(!companion_message.to_string().contains("/workspace/demo"));
let companion_transcript = get_json(app.clone(), "/api/companion/transcript").await;
assert_eq!(companion_transcript["total_items"], 0);
assert_eq!(companion_transcript["total"], 0);
let empty_window = get_json(app.clone(), "/api/companion/transcript?start=0&limit=0").await;
assert_eq!(
empty_window,
json!({
"state": "stopped",
"start": 0,
"limit": 0,
"total": 0,
"next": null,
"items": [],
})
);
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
assert!(
@@ -24336,7 +24356,7 @@ mod tests {
let workspace = get_json(app.clone(), "/api/workspace").await;
let workspace_companion = &workspace["extension_points"]["companion_console"];
assert_eq!(workspace_companion["status"], "disabled");
assert_eq!(workspace_companion["status"], "stopped");
assert_eq!(
workspace_companion["diagnostics"][0]["code"],
"companion_disabled"
@@ -24345,12 +24365,13 @@ mod tests {
workspace_companion["note"]
.as_str()
.unwrap()
.contains("auto-start has been removed")
.contains("typed diagnostics")
);
let status = get_json(app.clone(), "/api/companion/status").await;
assert_eq!(status["state"], "disabled");
assert_eq!(status["transport"]["completion"], "disabled");
assert_eq!(status["state"], "stopped");
assert_eq!(status["transport"]["mode"], "disabled");
assert_eq!(status["transport"]["available"], false);
assert!(status["worker"].is_null());
let response = post_json(
@@ -24360,13 +24381,14 @@ mod tests {
)
.await;
assert_eq!(response["state"], "rejected");
assert_eq!(response["diagnostics"][0]["code"], "companion_disabled");
assert!(response["user_item"].is_null());
assert!(response["assistant_item"].is_null());
assert!(response.get("accepted").is_none());
assert!(response.get("diagnostics").is_none());
assert!(response.get("user_item").is_none());
assert!(response.get("assistant_item").is_none());
let transcript = get_json(app.clone(), "/api/companion/transcript").await;
assert_eq!(transcript["state"], "disabled");
assert_eq!(transcript["total_items"], 0);
assert_eq!(transcript["state"], "stopped");
assert_eq!(transcript["total"], 0);
let workers = get_json(app, "/api/workers").await;
assert!(