Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion design-system/packages/ui/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ const OverlaySurface = forwardRef<HTMLDivElement, OverlaySurfaceProps>(function
ownerDocument,
});
useFocusScope({
active: open,
// A closed dialog mounts its surface on the presence commit. Start the
// focus scope only once that ref exists, including every subsequent open.
active: open && present,
autoFocus,
containerRef: surfaceRef,
initialFocusRef,
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ For shared GitHub sign-in and token redaction, use
For the matching cross-entry UI state, run
`pnpm --dir src/web-ui run test:run src/infrastructure/account-identity/AccountIdentityService.test.ts src/features/market-account/AccountIdentityControls.test.tsx src/app/components/RemoteConnectDialog/ensureAccountSession.test.ts`.

For side-question request compatibility, initial model selection, and optional
message metadata, use
`cargo test -p openbitfun-desktop --no-default-features --lib api::btw_api::tests`.

For skill discovery response compatibility and timeouts, use
`cargo test -p openbitfun-desktop --lib api::skill_api::tests`.
For companion pet manifest versions and package metadata, use
Expand Down
52 changes: 51 additions & 1 deletion src/apps/desktop/src/api/btw_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn desktop_btw_submission_policy() -> DialogSubmissionPolicy {
DialogSubmissionPolicy::for_source(DialogTriggerSource::DesktopUi)
}

#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BtwAskStreamRequest {
pub request_id: String,
Expand All @@ -35,6 +35,11 @@ pub struct BtwAskStreamRequest {
pub model_id: Option<String>,
#[serde(default)]
pub image_contexts: Option<Vec<ImageContextData>>,
/// Optional presentation metadata. The question remains readable by older hosts.
#[serde(default)]
pub user_message_metadata: Option<serde_json::Value>,
#[serde(default)]
pub initial_model_selection: Option<openbitfun_runtime_ports::AgentSessionModelSelection>,
}

#[derive(Debug, Clone, Serialize)]
Expand Down Expand Up @@ -116,6 +121,8 @@ pub async fn btw_ask_stream(
image_contexts,
request.parent_dialog_turn_id.as_deref(),
request.parent_turn_index,
request.user_message_metadata,
request.initial_model_selection,
)
.await
.map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -163,6 +170,49 @@ pub async fn btw_ask_stream(
mod tests {
use super::*;

#[test]
fn btw_request_reads_legacy_payloads_and_round_trips_optional_metadata() {
let legacy = serde_json::json!({
"requestId": "request-1",
"sessionId": "parent-1",
"childSessionId": "child-1",
"question": "A readable quote and question"
});
let request: BtwAskStreamRequest = serde_json::from_value(legacy.clone()).unwrap();
assert!(request.user_message_metadata.is_none());
assert!(request.initial_model_selection.is_none());
let round_trip: BtwAskStreamRequest =
serde_json::from_value(serde_json::to_value(request).unwrap()).unwrap();
assert_eq!(round_trip.question, legacy["question"]);

let metadata = serde_json::json!({
"composerPresentation": { "version": 1, "segments": [] },
"sessionReferences": [{ "sessionId": "reference-1" }],
"permission_mode": "default"
});
let mut current = legacy;
current["userMessageMetadata"] = metadata.clone();
current["initialModelSelection"] = serde_json::json!({
"modelId": "primary", "reasoningPreset": "high"
});
current["futureField"] = serde_json::json!(true);
let request: BtwAskStreamRequest = serde_json::from_value(current).unwrap();
let round_trip: BtwAskStreamRequest =
serde_json::from_value(serde_json::to_value(request).unwrap()).unwrap();
assert_eq!(round_trip.user_message_metadata, Some(metadata));
assert_eq!(
round_trip
.initial_model_selection
.unwrap()
.reasoning_preset
.as_deref(),
Some("high")
);
let auto: openbitfun_runtime_ports::AgentSessionModelSelection =
serde_json::from_value(serde_json::json!({ "modelId": "primary" })).unwrap();
assert!(auto.reasoning_preset.is_none());
}

#[test]
fn btw_turns_use_the_desktop_chat_output_surface() {
assert_eq!(
Expand Down
20 changes: 15 additions & 5 deletions src/crates/assembly/core/src/agentic/coordination/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10877,6 +10877,8 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
image_contexts: Option<Vec<ImageContextData>>,
parent_dialog_turn_id: Option<&str>,
parent_turn_index: Option<usize>,
user_message_metadata: Option<serde_json::Value>,
initial_model_selection: Option<openbitfun_runtime_ports::AgentSessionModelSelection>,
) -> OpenBitFunResult<String> {
if request_id.trim().is_empty() {
return Err(OpenBitFunError::Validation(
Expand Down Expand Up @@ -10910,7 +10912,14 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
)
.await?;

if let Some(model_id) = model_id
if let Some(selection) = initial_model_selection {
self.update_session_model_selection(
child_session_id,
&selection.model_id,
selection.reasoning_preset.as_deref(),
)
.await?;
} else if let Some(model_id) = model_id
.map(str::trim)
.filter(|model_id| !model_id.is_empty())
{
Expand All @@ -10920,10 +10929,11 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
}

let turn_id = format!("btw-turn-{}", request_id.trim());
let mut user_message_metadata = serde_json::json!({
"kind": "btw",
"parentSessionId": parent_session_id,
});
let mut user_message_metadata = user_message_metadata
.filter(serde_json::Value::is_object)
.unwrap_or_else(|| serde_json::json!({}));
user_message_metadata["kind"] = serde_json::json!("btw");
user_message_metadata["parentSessionId"] = serde_json::json!(parent_session_id);
if let Some(images) = image_contexts.as_ref().filter(|images| !images.is_empty()) {
user_message_metadata["images"] = serde_json::Value::Array(
images
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"digest": "fnv1a64:2851d2dc0482c8ac",
"digest": "fnv1a64:9b623ef2dd041ad8",
"retiredCommandPrefixes": [
{
"prefix": "lsp_",
Expand All @@ -11,6 +11,7 @@
"ids": [
"idempotent_dialog_submit",
"inline_image_attachments_v1",
"btw_initial_model_selection_v1",
"targeted_session_rollback",
"token_usage_statistics",
"miniapp_agent_context_files_v1",
Expand All @@ -27,6 +28,7 @@
"desktop": [
"idempotent_dialog_submit",
"inline_image_attachments_v1",
"btw_initial_model_selection_v1",
"targeted_session_rollback",
"token_usage_statistics",
"miniapp_agent_context_files_v1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub enum PeerHostCapability {
IdempotentDialogSubmit,
/// Inline imageContexts are durably prepared by the receiving Runtime.
InlineImageAttachmentsV1,
/// `btw_ask_stream` applies initial model and reasoning selection before the first turn.
BtwInitialModelSelectionV1,
/// Identity-based `rollback_session_to_turn` is implemented.
TargetedSessionRollback,
/// `get_token_usage_statistics` is implemented.
Expand Down Expand Up @@ -58,6 +60,7 @@ impl PeerHostCapability {
pub const ALL: &'static [PeerHostCapability] = &[
Self::IdempotentDialogSubmit,
Self::InlineImageAttachmentsV1,
Self::BtwInitialModelSelectionV1,
Self::TargetedSessionRollback,
Self::TokenUsageStatistics,
Self::MiniappAgentContextFilesV1,
Expand All @@ -77,6 +80,7 @@ impl PeerHostCapability {
match self {
Self::IdempotentDialogSubmit => "idempotent_dialog_submit",
Self::InlineImageAttachmentsV1 => "inline_image_attachments_v1",
Self::BtwInitialModelSelectionV1 => "btw_initial_model_selection_v1",
Self::TargetedSessionRollback => "targeted_session_rollback",
Self::TokenUsageStatistics => "token_usage_statistics",
Self::MiniappAgentContextFilesV1 => "miniapp_agent_context_files_v1",
Expand Down Expand Up @@ -170,6 +174,14 @@ mod tests {
advertised_by(PeerHostKind::Desktop).len(),
PeerHostCapability::ALL.len()
);
assert!(advertises(
PeerHostKind::Desktop,
PeerHostCapability::BtwInitialModelSelectionV1
));
assert!(!advertises(
PeerHostKind::Cli,
PeerHostCapability::BtwInitialModelSelectionV1
));
}

#[test]
Expand Down
7 changes: 7 additions & 0 deletions src/web-ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ deliberate layout transitions or animate virtualized content. Rely on CI for
full lint, build, and broad test coverage unless the local change specifically
needs it.

For Appearance descriptor changes, also exercise production registration; its ID
and property validation goes beyond the static DOM audit:

```bash
pnpm --dir src/web-ui run test:run src/infrastructure/appearance/registry/AppearanceRegistry.test.ts
```

For Session selection, presentation synchronization, and scene lifetime changes,
also run the focused state contracts:

Expand Down
2 changes: 2 additions & 0 deletions src/web-ui/src/flow_chat/components/ChatInput.appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const chatInputAppearanceDescriptor: AppearanceSurfaceDescriptor = {
{ id: 'image' },
{ id: 'imagePreview' },
{ id: 'imageRemove' },
{ id: 'attachment' },
{ id: 'attachmentRemove' },
{ id: 'placeholder' },
{ id: 'commandPicker' },
{ id: 'commandHeader' },
Expand Down
72 changes: 0 additions & 72 deletions src/web-ui/src/flow_chat/components/ChatInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -739,78 +739,6 @@
}
}

&__image-strip {
display: flex;
flex-wrap: wrap;
gap: var(--openbitfun-control-flow-chat-inline-gap);
margin-bottom: var(--openbitfun-control-flow-chat-inline-gap);
padding: 0.12rem 0;
}

&__image-chip {
position: relative;
width: 44px;
height: 44px;
border-radius: 6px;
overflow: visible;
background: var(--openbitfun-color-action-neutral-surface-hover);
border: 1px solid var(--openbitfun-color-border-subtle);
flex-shrink: 0;

&:hover .openbitfun-chat-input__image-chip-remove {
opacity: 1;
transform: scale(1);
}
}

&__image-chip-thumb {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 5px;
display: block;

&--placeholder {
display: flex;
align-items: center;
justify-content: center;
color: var(--openbitfun-color-content-secondary);
}
}

&__image-chip-remove {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
border-radius: 50%;
border: none;
background: var(--openbitfun-color-surface-raised);
color: var(--openbitfun-color-content-primary);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
box-shadow: 0 1px 3px color-mix(in srgb, var(--openbitfun-color-content-on-light) 35%, transparent);
opacity: 0;
transform: scale(0.85);
transition:
opacity 140ms ease,
transform 120ms cubic-bezier(0.23, 1, 0.32, 1);

&:hover {
background: var(--openbitfun-color-action-neutral-surface-hover);
}

&:focus-visible {
opacity: 1;
outline: 2px solid var(--openbitfun-color-accent-default);
outline-offset: 1px;
}
}

&__action-button {
flex-shrink: 0;
width: 20px;
Expand Down
Loading
Loading