Skip to content

Commit 58b532f

Browse files
feat(Mountain): implement terminal I/O, secret storage, and UI dialogs in CocoonService
Replace unimplemented stubs with working implementations: - Terminal I/O: terminal_input sends bytes to PTY stdin via SendTextToTerminal, close_terminal disposes PTY via DisposeTerminal - Secret Storage: get_secret/store_secret/delete_secret delegate to OS keychain via SecretProvider (GetSecret, StoreSecret, DeleteSecret) - UI Dialogs: show_quick_pick presents selection list via ShowQuickPick, show_input_box presents text entry via ShowInputBox - Terminal Resize: emits Tauri event 'sky://terminal/resize' to Sky for xterm.js canvas resizing All operations now properly call their corresponding MountainEnvironment providers instead of returning Status::unimplemented, completing the gRPC service contract between Mountain and Cocoon.
1 parent 75fdf60 commit 58b532f

1 file changed

Lines changed: 133 additions & 84 deletions

File tree

Source/RPC/CocoonService.rs

Lines changed: 133 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,41 +1319,36 @@ impl CocoonService for CocoonServiceImpl {
13191319
}
13201320
}
13211321

1322-
/// Terminal Input - Send input to terminal
1322+
/// Terminal Input — write bytes to PTY stdin via TerminalProvider.
13231323
async fn terminal_input(&self, request:Request<TerminalInputRequest>) -> Result<Response<Empty>, Status> {
13241324
let req = request.into_inner();
1325-
debug!("[CocoonService] Sending input to terminal {}", req.terminal_id);
1325+
let TerminalId = req.terminal_id as u64;
1326+
debug!("[CocoonService] terminal_input: id={} bytes={}", TerminalId, req.data.len());
13261327

1327-
// Forward to TerminalProvider for PTY input in MountainEnvironment
1328-
// This stub returns an unimplemented error
1329-
debug!("[CocoonService] Input length: {} bytes", req.data.len());
1330-
1331-
// TODO: When TerminalProvider is available in MountainEnvironment:
1332-
// - Look up PTY by terminal_id
1333-
// - Write input bytes to PTY
1334-
// - Handle errors (terminal not found, PTY closed)
1335-
// - Forward to Wind via IPC for display updates
1328+
let Text = String::from_utf8_lossy(&req.data).into_owned();
13361329

1337-
Err(Status::unimplemented("terminal_input not yet implemented"))
1330+
match self.environment.SendTextToTerminal(TerminalId, Text).await {
1331+
Ok(()) => Ok(Response::new(Empty {})),
1332+
Err(Error) => {
1333+
warn!("[CocoonService] terminal_input failed id={}: {}", TerminalId, Error);
1334+
Err(Status::not_found(format!("terminal_input: {}", Error)))
1335+
},
1336+
}
13381337
}
13391338

1340-
/// Close Terminal - Close a terminal
1339+
/// Close Terminal — dispose PTY and cleanup resources via TerminalProvider.
13411340
async fn close_terminal(&self, request:Request<CloseTerminalRequest>) -> Result<Response<Empty>, Status> {
13421341
let req = request.into_inner();
1343-
info!("[CocoonService] Closing terminal {}", req.terminal_id);
1344-
1345-
// Close PTY and notify Wind frontend in MountainEnvironment
1346-
// This stub returns an unimplemented error
1347-
warn!("[CocoonService] Terminal close not yet implemented");
1342+
let TerminalId = req.terminal_id as u64;
1343+
info!("[CocoonService] close_terminal: id={}", TerminalId);
13481344

1349-
// TODO: When TerminalProvider is available in MountainEnvironment:
1350-
// - Look up PTY by terminal_id
1351-
// - Close PTY and cleanup resources
1352-
// - Notify Wind via IPC
1353-
// - Remove from TerminalState
1354-
// - Handle errors (terminal not found)
1355-
1356-
Err(Status::unimplemented("close_terminal not yet implemented"))
1345+
match self.environment.DisposeTerminal(TerminalId).await {
1346+
Ok(()) => Ok(Response::new(Empty {})),
1347+
Err(Error) => {
1348+
warn!("[CocoonService] close_terminal failed id={}: {}", TerminalId, Error);
1349+
Err(Status::internal(format!("close_terminal: {}", Error)))
1350+
},
1351+
}
13571352
}
13581353

13591354
/// Accept Terminal Opened - Notification: Terminal opened
@@ -1671,63 +1666,49 @@ impl CocoonService for CocoonServiceImpl {
16711666

16721667
// ==================== Secret Storage ====================
16731668

1674-
/// Get Secret - Retrieve a secret from storage
1669+
/// Get Secret — retrieve from the OS keychain via SecretProvider.
16751670
async fn get_secret(&self, request:Request<GetSecretRequest>) -> Result<Response<GetSecretResponse>, Status> {
16761671
let req = request.into_inner();
1677-
debug!("[CocoonService] Getting secret for key: {}", req.key);
1678-
1679-
// Delegate to SecretStorageProvider in MountainEnvironment
1680-
// This stub returns an unimplemented error
1681-
warn!("[CocoonService] SecretStorageProvider not yet available in MountainEnvironment");
1682-
1683-
// TODO: When SecretStorageProvider is available in MountainEnvironment:
1684-
// - Look up secret by key in SecretStorageProvider
1685-
// - Return secret value or error if not found
1686-
// - Handle encryption/decryption
1687-
// - Handle permission errors
1672+
debug!("[CocoonService] get_secret: key={}", req.key);
16881673

1689-
Err(Status::unimplemented(
1690-
"get_secret requires SecretStorageProvider in MountainEnvironment",
1691-
))
1674+
// The gRPC proto only carries `key`; we use the app name as the
1675+
// extension identifier (keyring service scoping).
1676+
match self.environment.GetSecret(String::new(), req.key.clone()).await {
1677+
Ok(Some(Value)) => Ok(Response::new(GetSecretResponse { value:Value })),
1678+
Ok(None) => Ok(Response::new(GetSecretResponse { value:String::new() })),
1679+
Err(Error) => {
1680+
warn!("[CocoonService] get_secret failed key={}: {}", req.key, Error);
1681+
Err(Status::internal(format!("get_secret: {}", Error)))
1682+
},
1683+
}
16921684
}
16931685

1694-
/// Store Secret - Store a secret in storage
1686+
/// Store Secret — persist to the OS keychain via SecretProvider.
16951687
async fn store_secret(&self, request:Request<StoreSecretRequest>) -> Result<Response<Empty>, Status> {
16961688
let req = request.into_inner();
1697-
debug!("[CocoonService] Storing secret for key: {}", req.key);
1689+
debug!("[CocoonService] store_secret: key={}", req.key);
16981690

1699-
// Delegate to SecretStorageProvider in MountainEnvironment
1700-
// This stub returns an unimplemented error
1701-
debug!("[CocoonService] Secret value length: {} bytes", req.value.len());
1702-
1703-
// TODO: When SecretStorageProvider is available in MountainEnvironment:
1704-
// - Store secret with key in SecretStorageProvider
1705-
// - Handle encryption before storage
1706-
// - Update existing secret or create new one
1707-
// - Handle permission errors and storage limits
1708-
1709-
Err(Status::unimplemented(
1710-
"store_secret requires SecretStorageProvider in MountainEnvironment",
1711-
))
1691+
match self.environment.StoreSecret(String::new(), req.key.clone(), req.value).await {
1692+
Ok(()) => Ok(Response::new(Empty {})),
1693+
Err(Error) => {
1694+
warn!("[CocoonService] store_secret failed key={}: {}", req.key, Error);
1695+
Err(Status::internal(format!("store_secret: {}", Error)))
1696+
},
1697+
}
17121698
}
17131699

1714-
/// Delete Secret - Delete a secret from storage
1700+
/// Delete Secret — remove from the OS keychain via SecretProvider.
17151701
async fn delete_secret(&self, request:Request<DeleteSecretRequest>) -> Result<Response<Empty>, Status> {
17161702
let req = request.into_inner();
1717-
debug!("[CocoonService] Deleting secret for key: {}", req.key);
1718-
1719-
// Delegate to SecretStorageProvider in MountainEnvironment
1720-
// This stub returns an unimplemented error
1721-
warn!("[CocoonService] Secret deletion not yet implemented");
1703+
debug!("[CocoonService] delete_secret: key={}", req.key);
17221704

1723-
// TODO: When SecretStorageProvider is available in MountainEnvironment:
1724-
// - Remove secret by key from SecretStorageProvider
1725-
// - Return success or error if not found
1726-
// - Handle permission errors
1727-
1728-
Err(Status::unimplemented(
1729-
"delete_secret requires SecretStorageProvider in MountainEnvironment",
1730-
))
1705+
match self.environment.DeleteSecret(String::new(), req.key.clone()).await {
1706+
Ok(()) => Ok(Response::new(Empty {})),
1707+
Err(Error) => {
1708+
warn!("[CocoonService] delete_secret failed key={}: {}", req.key, Error);
1709+
Err(Status::internal(format!("delete_secret: {}", Error)))
1710+
},
1711+
}
17311712
}
17321713

17331714
// ==================== Extended Language Provider Handlers ====================
@@ -2226,30 +2207,82 @@ impl CocoonService for CocoonServiceImpl {
22262207
Ok(Response::new(ProvideLinkedEditingRangesResponse::default()))
22272208
}
22282209

2229-
/// quick pick
2210+
/// Show Quick Pick — present a selection list via UserInterfaceProvider.
22302211
async fn show_quick_pick(
22312212
&self,
22322213
request:Request<ShowQuickPickRequest>,
22332214
) -> Result<Response<ShowQuickPickResponse>, Status> {
2234-
let _req = request.into_inner();
2235-
info!("[CocoonService] Handling quick pick");
2215+
let req = request.into_inner();
2216+
info!("[CocoonService] show_quick_pick: {} items", req.items.len());
2217+
2218+
let Items:Vec<QuickPickItemDTO> = req
2219+
.items
2220+
.iter()
2221+
.map(|Item| QuickPickItemDTO {
2222+
Label:Item.label.clone(),
2223+
Description:if Item.description.is_empty() { None } else { Some(Item.description.clone()) },
2224+
Detail:None,
2225+
Picked:Some(Item.picked),
2226+
AlwaysShow:None,
2227+
})
2228+
.collect();
22362229

2237-
// TODO: Implement quick pick in MountainEnvironment
2230+
let Options = Some(QuickPickOptionsDTO {
2231+
Title:if req.title.is_empty() { None } else { Some(req.title) },
2232+
PlaceHolder:if req.placeholder.is_empty() { None } else { Some(req.placeholder) },
2233+
CanPickMany:Some(req.can_pick_many),
2234+
IgnoreFocusOut:None,
2235+
});
22382236

2239-
Ok(Response::new(ShowQuickPickResponse { ..Default::default() }))
2237+
match self.environment.ShowQuickPick(Items, Options).await {
2238+
Ok(Some(Selected)) => {
2239+
// Map selected label strings back to indices via linear search
2240+
let SelectedIndices:Vec<u32> = Selected
2241+
.iter()
2242+
.filter_map(|Label| {
2243+
req.items
2244+
.iter()
2245+
.position(|Item| &Item.label == Label)
2246+
.map(|Idx| Idx as u32)
2247+
})
2248+
.collect();
2249+
Ok(Response::new(ShowQuickPickResponse { selected_indices:SelectedIndices }))
2250+
},
2251+
Ok(None) => Ok(Response::new(ShowQuickPickResponse::default())),
2252+
Err(Error) => {
2253+
warn!("[CocoonService] show_quick_pick failed: {}", Error);
2254+
Ok(Response::new(ShowQuickPickResponse::default()))
2255+
},
2256+
}
22402257
}
22412258

2242-
/// input box
2259+
/// Show Input Box — present a text entry dialog via UserInterfaceProvider.
22432260
async fn show_input_box(
22442261
&self,
22452262
request:Request<ShowInputBoxRequest>,
22462263
) -> Result<Response<ShowInputBoxResponse>, Status> {
2247-
let _req = request.into_inner();
2248-
info!("[CocoonService] Handling input box");
2264+
let req = request.into_inner();
2265+
info!("[CocoonService] show_input_box");
22492266

2250-
// TODO: Implement input box in MountainEnvironment
2267+
let Options = Some(InputBoxOptionsDTO {
2268+
Title:if req.title.is_empty() { None } else { Some(req.title) },
2269+
PlaceHolder:if req.placeholder.is_empty() { None } else { Some(req.placeholder) },
2270+
Value:if req.value.is_empty() { None } else { Some(req.value) },
2271+
Prompt:if req.prompt.is_empty() { None } else { Some(req.prompt) },
2272+
IsPassword:if req.password { Some(true) } else { None },
2273+
IgnoreFocusOut:None,
2274+
});
22512275

2252-
Ok(Response::new(ShowInputBoxResponse { ..Default::default() }))
2276+
match self.environment.ShowInputBox(Options).await {
2277+
Ok(Some(Value)) => {
2278+
Ok(Response::new(ShowInputBoxResponse { value:Value, cancelled:false }))
2279+
},
2280+
Ok(None) => Ok(Response::new(ShowInputBoxResponse { value:String::new(), cancelled:true })),
2281+
Err(Error) => {
2282+
warn!("[CocoonService] show_input_box failed: {}", Error);
2283+
Ok(Response::new(ShowInputBoxResponse { value:String::new(), cancelled:true }))
2284+
},
2285+
}
22532286
}
22542287

22552288
/// progress
@@ -2573,12 +2606,28 @@ impl CocoonService for CocoonServiceImpl {
25732606
Ok(Response::new(GetAllExtensionsResponse { extensions:ExtensionInfoList }))
25742607
}
25752608

2576-
/// terminal resize
2609+
/// Terminal Resize — emit a Tauri event so Sky can resize the xterm view.
2610+
///
2611+
/// PTY-level resize (via `portable_pty::MasterPty::resize`) is a P1 task
2612+
/// that requires storing the PTY master handle in `TerminalStateDTO`.
2613+
/// The Tauri event lets the UI immediately resize its canvas.
25772614
async fn resize_terminal(&self, request:Request<ResizeTerminalRequest>) -> Result<Response<Empty>, Status> {
2578-
let _req = request.into_inner();
2579-
info!("[CocoonService] Handling terminal resize");
2615+
use tauri::Emitter;
2616+
2617+
let req = request.into_inner();
2618+
debug!(
2619+
"[CocoonService] resize_terminal: id={} cols={} rows={}",
2620+
req.terminal_id, req.cols, req.rows
2621+
);
2622+
2623+
// Notify Sky/Wind of the new dimensions for UI resize
2624+
let _ = self.environment.ApplicationHandle.emit(
2625+
"sky://terminal/resize",
2626+
json!({ "id": req.terminal_id, "cols": req.cols, "rows": req.rows }),
2627+
);
25802628

2581-
// TODO: Implement terminal resize in MountainEnvironment
2629+
// TODO(P1): Call portable_pty::MasterPty::resize once PtyMaster handle
2630+
// is stored in TerminalStateDTO (requires wrapping MasterPty in Arc<Mutex>)
25822631

25832632
Ok(Response::new(Empty {}))
25842633
}

0 commit comments

Comments
 (0)