assistant_context_adapter/
assistant_context_adapter.rs1use std::collections::VecDeque;
2use std::io::{self, Write};
3use std::sync::{Arc, Mutex};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use lcsa_core::{
7 Capability, ContextApi, PermissionRequest, Scope, SignalEnvelope, SignalSupport, SignalType,
8 StructuralSignal,
9};
10use serde_json::json;
11
12const MAX_EVENTS: usize = 20;
13
14#[derive(Clone, Default)]
15struct AdapterState {
16 recent_focus: Option<String>,
17 recent_selection: Option<String>,
18 recent_clipboard: Option<String>,
19 recent_events: VecDeque<String>,
20}
21
22fn main() -> Result<(), Box<dyn std::error::Error>> {
23 let mut api = ContextApi::new()?;
24 let state = Arc::new(Mutex::new(AdapterState::default()));
25
26 println!("LCSA Assistant Adapter Demo");
27 println!("Type a developer task and press Enter.");
28 println!("Commands: :help :context :grant-content :revoke-content :quit");
29 println!();
30
31 attach_signal_subscriptions(&mut api, Arc::clone(&state))?;
32
33 let stdin = io::stdin();
34 loop {
35 print!("ask> ");
36 io::stdout().flush()?;
37
38 let mut input = String::new();
39 stdin.read_line(&mut input)?;
40 let input = input.trim();
41 if input.is_empty() {
42 continue;
43 }
44
45 match input {
46 ":help" => print_help(),
47 ":context" => print_context_snapshot(&api, &state),
48 ":grant-content" => grant_clipboard_content(&mut api)?,
49 ":revoke-content" => {
50 let revoked = api.revoke_permission(Capability::ReadClipboardContent);
51 println!("clipboard content access revoked={revoked}");
52 }
53 ":quit" | ":exit" => break,
54 task => print_augmented_packet(&api, &state, task),
55 }
56 }
57
58 Ok(())
59}
60
61fn attach_signal_subscriptions(
62 api: &mut ContextApi,
63 state: Arc<Mutex<AdapterState>>,
64) -> Result<(), Box<dyn std::error::Error>> {
65 for signal_type in [
66 SignalType::Clipboard,
67 SignalType::Selection,
68 SignalType::Focus,
69 ] {
70 match api.signal_support(signal_type) {
71 SignalSupport::Supported => {
72 let state = Arc::clone(&state);
73 api.subscribe_enveloped(signal_type, move |envelope| {
74 ingest_envelope(&state, envelope);
75 })?;
76 println!("{signal_type:?}: subscribed");
77 }
78 unsupported => {
79 println!("{signal_type:?}: {unsupported:?}");
80 }
81 }
82 }
83
84 println!();
85 Ok(())
86}
87
88fn ingest_envelope(state: &Arc<Mutex<AdapterState>>, envelope: SignalEnvelope) {
89 let mut state = state.lock().expect("adapter state mutex poisoned");
90 let ts = unix_secs(envelope.emitted_at);
91
92 match envelope.payload {
93 StructuralSignal::Clipboard(signal) => {
94 let summary = format!(
95 "clipboard type={:?} bytes={} sensitive={} command={} source={}",
96 signal.content_type,
97 signal.size_bytes,
98 signal.likely_sensitive,
99 signal.likely_command,
100 signal.source_app
101 );
102 state.recent_clipboard = Some(summary.clone());
103 push_event(&mut state.recent_events, format!("{ts} {summary}"));
104 }
105 StructuralSignal::Selection(signal) => {
106 let preview = format!(
107 "selection type={:?} bytes={} editable={} source={}",
108 signal.content_type, signal.size_bytes, signal.is_editable, signal.source_app
109 );
110 state.recent_selection = Some(preview.clone());
111 push_event(&mut state.recent_events, format!("{ts} {preview}"));
112 }
113 StructuralSignal::Focus(signal) => {
114 let summary = format!(
115 "focus target={:?} editable={} source={}",
116 signal.target, signal.is_editable, signal.source_app
117 );
118 state.recent_focus = Some(summary.clone());
119 push_event(&mut state.recent_events, format!("{ts} {summary}"));
120 }
121 StructuralSignal::Filesystem(signal) => {
122 push_event(
123 &mut state.recent_events,
124 format!(
125 "{ts} filesystem {} {:?}",
126 signal.event_name(),
127 signal.action
128 ),
129 );
130 }
131 }
132}
133
134fn print_help() {
135 println!(":context show current signal snapshot");
136 println!(":grant-content grant clipboard content access for this session");
137 println!(":revoke-content revoke clipboard content access");
138 println!(":quit exit");
139}
140
141fn print_context_snapshot(api: &ContextApi, state: &Arc<Mutex<AdapterState>>) {
142 let snapshot = state.lock().expect("adapter state mutex poisoned").clone();
143 let packet = build_context_packet(api, &snapshot, "(snapshot)", None);
144 println!(
145 "{}",
146 serde_json::to_string_pretty(&packet).unwrap_or_default()
147 );
148}
149
150fn grant_clipboard_content(api: &mut ContextApi) -> Result<(), Box<dyn std::error::Error>> {
151 api.request_permission(PermissionRequest::new(
152 Capability::ReadClipboardContent,
153 Scope::Session,
154 "Enable content-level context in assistant adapter demo",
155 ))?;
156 println!("clipboard content access granted for current session");
157 Ok(())
158}
159
160fn print_augmented_packet(api: &ContextApi, state: &Arc<Mutex<AdapterState>>, task: &str) {
161 let snapshot = state.lock().expect("adapter state mutex poisoned").clone();
162 let clipboard_preview = api.read_clipboard_content().ok().map(|content| {
163 content.redacted_preview()
165 });
166
167 let packet = build_context_packet(api, &snapshot, task, clipboard_preview.clone());
168 println!();
169 println!("=== Augmented Context Packet ===");
170 println!(
171 "{}",
172 serde_json::to_string_pretty(&packet).unwrap_or_default()
173 );
174 println!();
175 println!("=== Prompt Preview ===");
176 println!(
177 "{}",
178 build_prompt_preview(task, &snapshot, clipboard_preview)
179 );
180 println!();
181}
182
183fn build_context_packet(
184 api: &ContextApi,
185 snapshot: &AdapterState,
186 task: &str,
187 clipboard_preview: Option<String>,
188) -> serde_json::Value {
189 json!({
190 "task": task,
191 "device": {
192 "id": api.device_context().device_id,
193 "platform": format!("{:?}", api.device_context().platform),
194 },
195 "capabilities": {
196 "clipboard": format!("{:?}", api.signal_support(SignalType::Clipboard)),
197 "selection": format!("{:?}", api.signal_support(SignalType::Selection)),
198 "focus": format!("{:?}", api.signal_support(SignalType::Focus)),
199 "clipboard_content_access": api.can_access(Capability::ReadClipboardContent),
200 },
201 "context": {
202 "recent_focus": snapshot.recent_focus,
203 "recent_selection": snapshot.recent_selection,
204 "recent_clipboard_signal": snapshot.recent_clipboard,
205 "clipboard_content_preview": clipboard_preview,
206 "recent_events": snapshot.recent_events.iter().take(8).cloned().collect::<Vec<_>>(),
207 },
208 "hints": derive_hints(snapshot),
209 })
210}
211
212fn build_prompt_preview(
213 task: &str,
214 snapshot: &AdapterState,
215 clipboard_preview: Option<String>,
216) -> String {
217 let mut lines = vec![
218 "You are a coding assistant helping with a local development task.".to_string(),
219 format!("Task: {task}"),
220 ];
221
222 if let Some(focus) = &snapshot.recent_focus {
223 lines.push(format!("Current focus: {focus}"));
224 }
225 if let Some(selection) = &snapshot.recent_selection {
226 lines.push(format!("Recent selection: {selection}"));
227 }
228 if let Some(clipboard) = &snapshot.recent_clipboard {
229 lines.push(format!("Recent clipboard signal: {clipboard}"));
230 }
231 if let Some(preview) = clipboard_preview {
232 lines.push(format!("Clipboard content preview: {preview}"));
233 }
234
235 for hint in derive_hints(snapshot) {
236 lines.push(format!("Hint: {hint}"));
237 }
238
239 lines.join("\n")
240}
241
242fn derive_hints(snapshot: &AdapterState) -> Vec<String> {
243 let mut hints = Vec::new();
244
245 if let Some(focus) = &snapshot.recent_focus {
246 let lowercase = focus.to_ascii_lowercase();
247 if lowercase.contains("terminal") {
248 hints.push("User is likely in a terminal-driven debugging flow.".to_string());
249 }
250 if lowercase.contains("browser") {
251 hints.push("User may be validating behavior in a browser.".to_string());
252 }
253 }
254
255 if let Some(selection) = &snapshot.recent_selection {
256 if selection.contains("Code") {
257 hints.push(
258 "Selected text looks code-like; prioritize code-aware suggestions.".to_string(),
259 );
260 }
261 }
262
263 if let Some(clipboard) = &snapshot.recent_clipboard {
264 if clipboard.contains("sensitive=true") {
265 hints
266 .push("Clipboard likely contains sensitive data; avoid verbatim echo.".to_string());
267 }
268 if clipboard.contains("command=true") {
269 hints.push(
270 "Clipboard resembles a shell command; offer safe command guidance.".to_string(),
271 );
272 }
273 }
274
275 hints
276}
277
278fn push_event(events: &mut VecDeque<String>, line: String) {
279 events.push_front(line);
280 while events.len() > MAX_EVENTS {
281 let _ = events.pop_back();
282 }
283}
284
285fn unix_secs(ts: SystemTime) -> u64 {
286 ts.duration_since(UNIX_EPOCH)
287 .map(|duration| duration.as_secs())
288 .unwrap_or_default()
289}