1use crate::desktop::{
2 DesktopContextResponse, DesktopDaemonRequest, DesktopErrorEnvelope, DesktopHistoryResponse,
3 DesktopImportSessionRequest, DesktopImportSessionResponse, DesktopLifecycleActionDto,
4 DesktopMemoryActionRequest, DesktopMemoryDraftRequest, DesktopMetadataDto,
5 DesktopPromptOptimizeRequest, DesktopPromptOptimizeResponse, DesktopRecordLookupRequest,
6 DesktopRecordResponse, DesktopRouteRequest, DesktopService, DesktopSessionActionRequest,
7 DesktopSessionActionResponse, DesktopSessionBrowserRequest, DesktopSessionBrowserResponse,
8 DesktopSessionDetailRequest, DesktopSessionDetailResponse, DesktopStatusRequest,
9 DesktopStatusResponse, DesktopWakeupRequest, DesktopWakeupResponse, DesktopWikiIndexRequest,
10 DesktopWikiIndexResponse, DesktopWikiLintRequest, DesktopWikiLintResponse,
11 DesktopWorkbenchRequest, DesktopWorkbenchResponse, DesktopWriteResponse,
12};
13use crate::domain::{MemoryScope, OutputFormat, TargetTool, WakeupProfile};
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16
17pub type TauriCommandResult<T> = Result<T, DesktopErrorEnvelope>;
18
19#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
20pub struct TauriContextCommand {
21 pub config_path: PathBuf,
22 pub vault_root_override: Option<PathBuf>,
23 pub cwd: PathBuf,
24 pub task: String,
25 #[serde(default)]
26 pub files: Vec<String>,
27 pub target: TargetTool,
28 pub format: OutputFormat,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
32pub struct TauriWakeupCommand {
33 pub config_path: PathBuf,
34 pub vault_root_override: Option<PathBuf>,
35 pub cwd: PathBuf,
36 pub task: String,
37 #[serde(default)]
38 pub files: Vec<String>,
39 pub target: TargetTool,
40 pub profile: WakeupProfile,
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
44pub struct TauriPromptOptimizeCommand {
45 pub config_path: PathBuf,
46 pub vault_root_override: Option<PathBuf>,
47 pub cwd: PathBuf,
48 pub task: String,
49 #[serde(default)]
50 pub files: Vec<String>,
51 pub target: TargetTool,
52 pub profile: WakeupProfile,
53 pub provider: Option<String>,
54 pub session_id: Option<String>,
55}
56
57#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
58pub struct TauriDaemonCommand {
59 pub enabled: bool,
60 pub daemon_bin: Option<PathBuf>,
61}
62
63#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
64pub struct TauriWorkbenchCommand {
65 pub config_path: PathBuf,
66 pub daemon: Option<TauriDaemonCommand>,
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
70pub struct TauriRecordCommand {
71 pub config_path: PathBuf,
72 pub record_id: String,
73 pub daemon: Option<TauriDaemonCommand>,
74}
75
76#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
77pub struct TauriSessionBrowserCommand {
78 pub config_path: PathBuf,
79 pub page: Option<usize>,
80 pub per_page: Option<usize>,
81 pub provider: Option<String>,
82}
83
84#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
85pub struct TauriSessionDetailCommand {
86 pub config_path: PathBuf,
87 pub session_id: String,
88 pub message_offset: Option<usize>,
89 pub message_limit: Option<usize>,
90}
91
92#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
93pub struct TauriSessionActionCommand {
94 pub config_path: PathBuf,
95 pub session_id: String,
96}
97
98#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
99pub struct TauriStatusCommand {
100 pub config_path: PathBuf,
101 pub vault_root_override: Option<PathBuf>,
102 pub cwd: PathBuf,
103}
104
105#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
106pub struct TauriImportSessionCommand {
107 pub config_path: PathBuf,
108 pub provider: String,
109 pub session_id: String,
110 #[serde(default)]
111 pub apply: bool,
112 #[serde(default)]
113 pub actor: Option<String>,
114}
115
116#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
117pub struct TauriMemoryDraftCommand {
118 pub config_path: PathBuf,
119 pub title: String,
120 pub summary: String,
121 pub memory_type: String,
122 pub scope: MemoryScope,
123 pub source_ref: String,
124 pub project_id: Option<String>,
125 pub user_id: Option<String>,
126 pub sensitivity: Option<String>,
127 #[serde(default)]
128 pub metadata: DesktopMetadataDto,
129 #[serde(default)]
131 pub entities: Vec<String>,
132 #[serde(default)]
133 pub tags: Vec<String>,
134 #[serde(default)]
135 pub triggers: Vec<String>,
136 #[serde(default)]
137 pub related_files: Vec<String>,
138 #[serde(default)]
139 pub related_records: Vec<String>,
140 #[serde(default)]
141 pub supersedes: Option<String>,
142 #[serde(default)]
143 pub applies_to: Vec<String>,
144 #[serde(default)]
145 pub valid_until: Option<String>,
146}
147
148#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
149pub struct TauriMemoryActionCommand {
150 pub config_path: PathBuf,
151 pub record_id: String,
152 pub action: DesktopLifecycleActionDto,
153 #[serde(default)]
154 pub metadata: DesktopMetadataDto,
155}
156
157pub fn desktop_run_context(
158 command: TauriContextCommand,
159) -> TauriCommandResult<DesktopContextResponse> {
160 DesktopService::new().run_context(command.into_request())
161}
162
163pub fn desktop_build_wakeup(
164 command: TauriWakeupCommand,
165) -> TauriCommandResult<DesktopWakeupResponse> {
166 DesktopService::new().build_wakeup(command.into_request())
167}
168
169pub fn desktop_optimize_prompt(
170 command: TauriPromptOptimizeCommand,
171) -> TauriCommandResult<DesktopPromptOptimizeResponse> {
172 DesktopService::new().optimize_prompt(command.into_request())
173}
174
175pub fn desktop_load_workbench(
176 command: TauriWorkbenchCommand,
177) -> TauriCommandResult<DesktopWorkbenchResponse> {
178 DesktopService::new().load_workbench(command.into_request())
179}
180
181pub fn desktop_get_record(
182 command: TauriRecordCommand,
183) -> TauriCommandResult<Option<DesktopRecordResponse>> {
184 DesktopService::new().get_record(command.into_request())
185}
186
187pub fn desktop_get_history(
188 command: TauriRecordCommand,
189) -> TauriCommandResult<DesktopHistoryResponse> {
190 DesktopService::new().get_history(command.into_request())
191}
192
193pub fn desktop_browse_sessions(
194 command: TauriSessionBrowserCommand,
195) -> TauriCommandResult<DesktopSessionBrowserResponse> {
196 DesktopService::new().browse_sessions(command.into_request())
197}
198
199pub fn desktop_get_session(
200 command: TauriSessionDetailCommand,
201) -> TauriCommandResult<Option<DesktopSessionDetailResponse>> {
202 DesktopService::new().get_session(command.into_request())
203}
204
205pub fn desktop_collect_status(
206 command: TauriStatusCommand,
207) -> TauriCommandResult<DesktopStatusResponse> {
208 DesktopService::new().collect_status(command.into_request())
209}
210
211pub fn desktop_continue_session(
212 command: TauriSessionActionCommand,
213) -> TauriCommandResult<DesktopSessionActionResponse> {
214 DesktopService::new().continue_session(command.into_request())
215}
216
217pub fn desktop_delete_session(
218 command: TauriSessionActionCommand,
219) -> TauriCommandResult<DesktopSessionActionResponse> {
220 DesktopService::new().delete_session(command.into_request())
221}
222
223pub fn desktop_record_manual(
224 command: TauriMemoryDraftCommand,
225) -> TauriCommandResult<DesktopWriteResponse> {
226 DesktopService::new().record_manual(command.into_request())
227}
228
229pub fn desktop_propose_memory(
230 command: TauriMemoryDraftCommand,
231) -> TauriCommandResult<DesktopWriteResponse> {
232 DesktopService::new().propose_memory(command.into_request())
233}
234
235pub fn desktop_apply_memory_action(
236 command: TauriMemoryActionCommand,
237) -> TauriCommandResult<DesktopWriteResponse> {
238 DesktopService::new().apply_memory_action(command.into_request())
239}
240
241pub fn desktop_import_session(
242 command: TauriImportSessionCommand,
243) -> TauriCommandResult<DesktopImportSessionResponse> {
244 DesktopService::new().import_session(command.into_request())
245}
246
247pub fn desktop_wiki_lint(
248 request: DesktopWikiLintRequest,
249) -> TauriCommandResult<DesktopWikiLintResponse> {
250 DesktopService::new().wiki_lint(request)
251}
252
253pub fn desktop_read_wiki_index(
254 request: DesktopWikiIndexRequest,
255) -> TauriCommandResult<DesktopWikiIndexResponse> {
256 DesktopService::new().read_wiki_index(request)
257}
258
259impl TauriContextCommand {
260 fn into_request(self) -> DesktopRouteRequest {
261 DesktopRouteRequest {
262 config_path: self.config_path,
263 vault_root_override: self.vault_root_override,
264 cwd: self.cwd,
265 task: self.task,
266 files: self.files,
267 target: self.target,
268 format: self.format,
269 }
270 }
271}
272
273impl TauriWakeupCommand {
274 fn into_request(self) -> DesktopWakeupRequest {
275 DesktopWakeupRequest {
276 config_path: self.config_path,
277 vault_root_override: self.vault_root_override,
278 cwd: self.cwd,
279 task: self.task,
280 files: self.files,
281 target: self.target,
282 profile: self.profile,
283 }
284 }
285}
286
287impl TauriPromptOptimizeCommand {
288 fn into_request(self) -> DesktopPromptOptimizeRequest {
289 DesktopPromptOptimizeRequest {
290 config_path: self.config_path,
291 vault_root_override: self.vault_root_override,
292 cwd: self.cwd,
293 task: self.task,
294 files: self.files,
295 target: self.target,
296 profile: self.profile,
297 provider: self.provider,
298 session_id: self.session_id,
299 }
300 }
301}
302
303impl TauriWorkbenchCommand {
304 fn into_request(self) -> DesktopWorkbenchRequest {
305 DesktopWorkbenchRequest {
306 config_path: self.config_path,
307 daemon: self.daemon.map(TauriDaemonCommand::into_request),
308 }
309 }
310}
311
312impl TauriRecordCommand {
313 fn into_request(self) -> DesktopRecordLookupRequest {
314 DesktopRecordLookupRequest {
315 config_path: self.config_path,
316 record_id: self.record_id,
317 daemon: self.daemon.map(TauriDaemonCommand::into_request),
318 }
319 }
320}
321
322impl TauriDaemonCommand {
323 fn into_request(self) -> DesktopDaemonRequest {
324 DesktopDaemonRequest {
325 enabled: self.enabled,
326 daemon_bin: self.daemon_bin,
327 }
328 }
329}
330
331impl TauriSessionBrowserCommand {
332 fn into_request(self) -> DesktopSessionBrowserRequest {
333 DesktopSessionBrowserRequest {
334 config_path: self.config_path,
335 page: self.page.unwrap_or(1),
336 per_page: self.per_page.unwrap_or(10),
337 provider: self.provider,
338 }
339 }
340}
341
342impl TauriSessionDetailCommand {
343 fn into_request(self) -> DesktopSessionDetailRequest {
344 DesktopSessionDetailRequest {
345 config_path: self.config_path,
346 session_id: self.session_id,
347 message_offset: self.message_offset,
348 message_limit: self.message_limit,
349 }
350 }
351}
352
353impl TauriStatusCommand {
354 fn into_request(self) -> DesktopStatusRequest {
355 DesktopStatusRequest {
356 config_path: self.config_path,
357 vault_root_override: self.vault_root_override,
358 cwd: self.cwd,
359 }
360 }
361}
362
363impl TauriSessionActionCommand {
364 fn into_request(self) -> DesktopSessionActionRequest {
365 DesktopSessionActionRequest {
366 config_path: self.config_path,
367 session_id: self.session_id,
368 }
369 }
370}
371
372impl TauriMemoryDraftCommand {
373 fn into_request(self) -> DesktopMemoryDraftRequest {
374 DesktopMemoryDraftRequest {
375 config_path: self.config_path,
376 title: self.title,
377 summary: self.summary,
378 memory_type: self.memory_type,
379 scope: self.scope,
380 source_ref: self.source_ref,
381 project_id: self.project_id,
382 user_id: self.user_id,
383 sensitivity: self.sensitivity,
384 metadata: self.metadata,
385 entities: self.entities,
386 tags: self.tags,
387 triggers: self.triggers,
388 related_files: self.related_files,
389 related_records: self.related_records,
390 supersedes: self.supersedes,
391 applies_to: self.applies_to,
392 valid_until: self.valid_until,
393 }
394 }
395}
396
397impl TauriMemoryActionCommand {
398 fn into_request(self) -> DesktopMemoryActionRequest {
399 DesktopMemoryActionRequest {
400 config_path: self.config_path,
401 record_id: self.record_id,
402 action: self.action,
403 metadata: self.metadata,
404 }
405 }
406}
407
408impl TauriImportSessionCommand {
409 fn into_request(self) -> DesktopImportSessionRequest {
410 DesktopImportSessionRequest {
411 config_path: self.config_path,
412 provider: self.provider,
413 session_id: self.session_id,
414 apply: self.apply,
415 actor: self.actor,
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::{
423 TauriContextCommand, TauriDaemonCommand, TauriMemoryActionCommand, TauriMemoryDraftCommand,
424 TauriRecordCommand, TauriWakeupCommand, TauriWorkbenchCommand, desktop_apply_memory_action,
425 desktop_build_wakeup, desktop_get_history, desktop_get_record, desktop_load_workbench,
426 desktop_propose_memory, desktop_run_context,
427 };
428 use crate::desktop::{DesktopLifecycleActionDto, DesktopMetadataDto};
429 use crate::domain::{
430 MemoryLifecycleState, MemoryScope, OutputFormat, TargetTool, WakeupProfile,
431 };
432 use std::fs;
433 use tempfile::tempdir;
434
435 fn setup_workspace() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
436 let temp = tempdir().unwrap();
437 let vault_root = temp.path().join("vault");
438 let repo_root = temp.path().join("repo");
439 fs::create_dir_all(vault_root.join("10-Projects")).unwrap();
440 fs::create_dir_all(&repo_root).unwrap();
441 fs::write(
442 vault_root.join("10-Projects/spool.md"),
443 "---\ntitle: spool\nmemory_type: project\n---\n\nDesktop tauri command test note.\n",
444 )
445 .unwrap();
446 let config_path = temp.path().join("spool.toml");
447 fs::write(
448 &config_path,
449 format!(
450 "[vault]\nroot = \"{}\"\n\n[output]\ndefault_format = \"markdown\"\nmax_chars = 4000\nmax_notes = 8\n\n[[projects]]\nid = \"spool\"\nname = \"spool\"\nrepo_paths = [\"{}\"]\nnote_roots = [\"10-Projects\"]\n",
451 vault_root.display(),
452 repo_root.display(),
453 ),
454 )
455 .unwrap();
456 (temp, config_path, repo_root)
457 }
458
459 #[test]
460 fn tauri_commands_should_parse_inputs_and_run_context_and_wakeup() {
461 let (_temp, config_path, repo_root) = setup_workspace();
462
463 let context = desktop_run_context(TauriContextCommand {
464 config_path: config_path.clone(),
465 vault_root_override: None,
466 cwd: repo_root.clone(),
467 task: "summarize current project context".to_string(),
468 files: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()],
469 target: TargetTool::Claude,
470 format: OutputFormat::Markdown,
471 })
472 .unwrap();
473 assert_eq!(context.bundle.route.debug.note_count, 1);
474
475 let wakeup = desktop_build_wakeup(TauriWakeupCommand {
476 config_path,
477 vault_root_override: None,
478 cwd: repo_root,
479 task: "prepare restart packet".to_string(),
480 files: Vec::new(),
481 target: TargetTool::Claude,
482 profile: WakeupProfile::Project,
483 })
484 .unwrap();
485 assert_eq!(wakeup.packet.profile, WakeupProfile::Project);
486 }
487
488 #[test]
489 fn tauri_commands_should_cover_lifecycle_read_and_write_flows() {
490 let (_temp, config_path, _repo_root) = setup_workspace();
491
492 let created = desktop_propose_memory(TauriMemoryDraftCommand {
493 config_path: config_path.clone(),
494 title: "测试偏好".to_string(),
495 summary: "先 smoke 再收口".to_string(),
496 memory_type: "workflow".to_string(),
497 scope: MemoryScope::User,
498 source_ref: "session:1".to_string(),
499 project_id: None,
500 user_id: Some("long".to_string()),
501 sensitivity: None,
502 metadata: DesktopMetadataDto {
503 actor: Some("desktop".to_string()),
504 reason: Some("captured from tauri command".to_string()),
505 evidence_refs: vec!["session:1".to_string(), "obsidian://workflow".to_string()],
506 },
507 entities: Vec::new(),
508 tags: Vec::new(),
509 triggers: Vec::new(),
510 related_files: Vec::new(),
511 related_records: Vec::new(),
512 supersedes: None,
513 applies_to: Vec::new(),
514 valid_until: None,
515 })
516 .unwrap();
517 assert_eq!(created.entry.record.state, MemoryLifecycleState::Candidate);
518
519 let workbench = desktop_load_workbench(TauriWorkbenchCommand {
520 config_path: config_path.clone(),
521 daemon: Some(TauriDaemonCommand {
522 enabled: false,
523 daemon_bin: None,
524 }),
525 })
526 .unwrap();
527 assert_eq!(workbench.snapshot.pending_review.len(), 1);
528
529 let record = desktop_get_record(TauriRecordCommand {
530 config_path: config_path.clone(),
531 record_id: created.entry.record_id.clone(),
532 daemon: None,
533 })
534 .unwrap()
535 .unwrap();
536 assert_eq!(record.record.record.title, "测试偏好");
537
538 let action = desktop_apply_memory_action(TauriMemoryActionCommand {
539 config_path: config_path.clone(),
540 record_id: created.entry.record_id.clone(),
541 action: DesktopLifecycleActionDto::Accept,
542 metadata: DesktopMetadataDto::default(),
543 })
544 .unwrap();
545 assert_eq!(action.entry.record.state, MemoryLifecycleState::Accepted);
546
547 let history = desktop_get_history(TauriRecordCommand {
548 config_path,
549 record_id: created.entry.record_id,
550 daemon: None,
551 })
552 .unwrap();
553 assert_eq!(history.history.len(), 2);
554 }
555}