1use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "backend")]
12pub mod crypto;
13#[cfg(feature = "backend")]
14pub mod db;
15pub mod deploy;
16pub mod oauth;
17#[cfg(feature = "backend")]
18pub mod service;
19
20pub use opensession_core::trace::{
22 Agent, Content, ContentBlock, Event, EventType, Session, SessionContext, Stats,
23};
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
29#[serde(rename_all = "snake_case")]
30#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
31#[cfg_attr(feature = "ts", ts(export))]
32pub enum SortOrder {
33 #[default]
34 Recent,
35 Popular,
36 Longest,
37}
38
39impl SortOrder {
40 pub fn as_str(&self) -> &str {
41 match self {
42 Self::Recent => "recent",
43 Self::Popular => "popular",
44 Self::Longest => "longest",
45 }
46 }
47}
48
49impl std::fmt::Display for SortOrder {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.write_str(self.as_str())
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
57#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
58#[cfg_attr(feature = "ts", ts(export))]
59pub enum TimeRange {
60 #[serde(rename = "24h")]
61 Hours24,
62 #[serde(rename = "7d")]
63 Days7,
64 #[serde(rename = "30d")]
65 Days30,
66 #[default]
67 #[serde(rename = "all")]
68 All,
69}
70
71impl TimeRange {
72 pub fn as_str(&self) -> &str {
73 match self {
74 Self::Hours24 => "24h",
75 Self::Days7 => "7d",
76 Self::Days30 => "30d",
77 Self::All => "all",
78 }
79 }
80}
81
82impl std::fmt::Display for TimeRange {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.write_str(self.as_str())
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90#[serde(rename_all = "snake_case")]
91#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
92#[cfg_attr(feature = "ts", ts(export))]
93pub enum LinkType {
94 Handoff,
95 Related,
96 Parent,
97 Child,
98}
99
100impl LinkType {
101 pub fn as_str(&self) -> &str {
102 match self {
103 Self::Handoff => "handoff",
104 Self::Related => "related",
105 Self::Parent => "parent",
106 Self::Child => "child",
107 }
108 }
109}
110
111impl std::fmt::Display for LinkType {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.write_str(self.as_str())
114 }
115}
116
117pub fn saturating_i64(v: u64) -> i64 {
121 i64::try_from(v).unwrap_or(i64::MAX)
122}
123
124#[derive(Debug, Serialize, Deserialize)]
128#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
129#[cfg_attr(feature = "ts", ts(export))]
130pub struct AuthRegisterRequest {
131 pub email: String,
132 pub password: String,
133 pub nickname: String,
134}
135
136#[derive(Debug, Serialize, Deserialize)]
138#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
139#[cfg_attr(feature = "ts", ts(export))]
140pub struct LoginRequest {
141 pub email: String,
142 pub password: String,
143}
144
145#[derive(Debug, Serialize, Deserialize)]
147#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
148#[cfg_attr(feature = "ts", ts(export))]
149pub struct AuthTokenResponse {
150 pub access_token: String,
151 pub refresh_token: String,
152 pub expires_in: u64,
153 pub user_id: String,
154 pub nickname: String,
155}
156
157#[derive(Debug, Serialize, Deserialize)]
159#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
160#[cfg_attr(feature = "ts", ts(export))]
161pub struct RefreshRequest {
162 pub refresh_token: String,
163}
164
165#[derive(Debug, Serialize, Deserialize)]
167#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
168#[cfg_attr(feature = "ts", ts(export))]
169pub struct LogoutRequest {
170 pub refresh_token: String,
171}
172
173#[derive(Debug, Serialize, Deserialize)]
175#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
176#[cfg_attr(feature = "ts", ts(export))]
177pub struct ChangePasswordRequest {
178 pub current_password: String,
179 pub new_password: String,
180}
181
182#[derive(Debug, Serialize, Deserialize)]
184#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
185#[cfg_attr(feature = "ts", ts(export))]
186pub struct VerifyResponse {
187 pub user_id: String,
188 pub nickname: String,
189}
190
191#[derive(Debug, Serialize, Deserialize)]
193#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
194#[cfg_attr(feature = "ts", ts(export))]
195pub struct UserSettingsResponse {
196 pub user_id: String,
197 pub nickname: String,
198 pub created_at: String,
199 pub email: Option<String>,
200 pub avatar_url: Option<String>,
201 #[serde(default)]
203 pub oauth_providers: Vec<oauth::LinkedProvider>,
204}
205
206#[derive(Debug, Serialize, Deserialize)]
208#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
209#[cfg_attr(feature = "ts", ts(export))]
210pub struct OkResponse {
211 pub ok: bool,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
216#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
217#[cfg_attr(feature = "ts", ts(export))]
218pub struct IssueApiKeyResponse {
219 pub api_key: String,
220}
221
222#[derive(Debug, Serialize, Deserialize)]
224#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
225#[cfg_attr(feature = "ts", ts(export))]
226pub struct GitCredentialSummary {
227 pub id: String,
228 pub label: String,
229 pub host: String,
230 pub path_prefix: String,
231 pub header_name: String,
232 pub created_at: String,
233 pub updated_at: String,
234 pub last_used_at: Option<String>,
235}
236
237#[derive(Debug, Serialize, Deserialize)]
239#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
240#[cfg_attr(feature = "ts", ts(export))]
241pub struct ListGitCredentialsResponse {
242 #[serde(default)]
243 pub credentials: Vec<GitCredentialSummary>,
244}
245
246#[derive(Debug, Serialize, Deserialize)]
248#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
249#[cfg_attr(feature = "ts", ts(export))]
250pub struct CreateGitCredentialRequest {
251 pub label: String,
252 pub host: String,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub path_prefix: Option<String>,
255 pub header_name: String,
256 pub header_value: String,
257}
258
259#[derive(Debug, Serialize)]
261#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
262#[cfg_attr(feature = "ts", ts(export))]
263pub struct OAuthLinkResponse {
264 pub url: String,
265}
266
267#[derive(Debug, Serialize, Deserialize)]
271pub struct UploadRequest {
272 pub session: Session,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub body_url: Option<String>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub linked_session_ids: Option<Vec<String>>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub git_remote: Option<String>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub git_branch: Option<String>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub git_commit: Option<String>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub git_repo_name: Option<String>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub pr_number: Option<i64>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub pr_url: Option<String>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub score_plugin: Option<String>,
291}
292
293#[derive(Debug, Serialize, Deserialize)]
295#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
296#[cfg_attr(feature = "ts", ts(export))]
297pub struct UploadResponse {
298 pub id: String,
299 pub url: String,
300 #[serde(default)]
301 pub session_score: i64,
302 #[serde(default = "default_score_plugin")]
303 pub score_plugin: String,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
309#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
310#[cfg_attr(feature = "ts", ts(export))]
311pub struct SessionSummary {
312 pub id: String,
313 pub user_id: Option<String>,
314 pub nickname: Option<String>,
315 pub tool: String,
316 pub agent_provider: Option<String>,
317 pub agent_model: Option<String>,
318 pub title: Option<String>,
319 pub description: Option<String>,
320 pub tags: Option<String>,
322 pub created_at: String,
323 pub uploaded_at: String,
324 pub message_count: i64,
325 pub task_count: i64,
326 pub event_count: i64,
327 pub duration_seconds: i64,
328 pub total_input_tokens: i64,
329 pub total_output_tokens: i64,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub git_remote: Option<String>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub git_branch: Option<String>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub git_commit: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub git_repo_name: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub pr_number: Option<i64>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub pr_url: Option<String>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub working_directory: Option<String>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub files_modified: Option<String>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub files_read: Option<String>,
348 #[serde(default)]
349 pub has_errors: bool,
350 #[serde(default = "default_max_active_agents")]
351 pub max_active_agents: i64,
352 #[serde(default)]
353 pub session_score: i64,
354 #[serde(default = "default_score_plugin")]
355 pub score_plugin: String,
356}
357
358#[derive(Debug, Serialize, Deserialize)]
360#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
361#[cfg_attr(feature = "ts", ts(export))]
362pub struct SessionListResponse {
363 pub sessions: Vec<SessionSummary>,
364 pub total: i64,
365 pub page: u32,
366 pub per_page: u32,
367}
368
369pub const DESKTOP_IPC_CONTRACT_VERSION: &str = "desktop-ipc-v6";
371
372#[derive(Debug, Deserialize)]
374#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
375#[cfg_attr(feature = "ts", ts(export))]
376pub struct SessionListQuery {
377 #[serde(default = "default_page")]
378 pub page: u32,
379 #[serde(default = "default_per_page")]
380 pub per_page: u32,
381 pub search: Option<String>,
382 pub tool: Option<String>,
383 pub git_repo_name: Option<String>,
384 pub sort: Option<SortOrder>,
386 pub time_range: Option<TimeRange>,
388}
389
390#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
393#[cfg_attr(feature = "ts", ts(export))]
394pub struct DesktopSessionListQuery {
395 pub page: Option<String>,
396 pub per_page: Option<String>,
397 pub search: Option<String>,
398 pub tool: Option<String>,
399 pub git_repo_name: Option<String>,
400 pub sort: Option<String>,
401 pub time_range: Option<String>,
402 pub force_refresh: Option<bool>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
408#[cfg_attr(feature = "ts", ts(export))]
409pub struct SessionRepoListResponse {
410 pub repos: Vec<String>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
415#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
416#[cfg_attr(feature = "ts", ts(export))]
417pub struct DesktopHandoffBuildRequest {
418 pub session_id: String,
419 pub pin_latest: bool,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
425#[cfg_attr(feature = "ts", ts(export))]
426pub struct DesktopHandoffBuildResponse {
427 pub artifact_uri: String,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub pinned_alias: Option<String>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub download_file_name: Option<String>,
432 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub download_content: Option<String>,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
439#[cfg_attr(feature = "ts", ts(export))]
440pub struct DesktopQuickShareRequest {
441 pub session_id: String,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub remote: Option<String>,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
448#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
449#[cfg_attr(feature = "ts", ts(export))]
450pub struct DesktopQuickShareResponse {
451 pub source_uri: String,
452 pub shared_uri: String,
453 pub remote: String,
454 pub push_cmd: String,
455 #[serde(default)]
456 pub pushed: bool,
457 #[serde(default)]
458 pub auto_push_consent: bool,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
464#[cfg_attr(feature = "ts", ts(export))]
465pub struct DesktopContractVersionResponse {
466 pub version: String,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
471#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
472#[cfg_attr(feature = "ts", ts(export))]
473pub struct DesktopRuntimeSettingsResponse {
474 pub session_default_view: String,
475 pub summary: DesktopRuntimeSummarySettings,
476 pub vector_search: DesktopRuntimeVectorSearchSettings,
477 pub change_reader: DesktopRuntimeChangeReaderSettings,
478 pub lifecycle: DesktopRuntimeLifecycleSettings,
479 pub ui_constraints: DesktopRuntimeSummaryUiConstraints,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize, Default)]
484#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
485#[cfg_attr(feature = "ts", ts(export))]
486pub struct DesktopRuntimeSettingsUpdateRequest {
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub session_default_view: Option<String>,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub summary: Option<DesktopRuntimeSummarySettingsUpdate>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub vector_search: Option<DesktopRuntimeVectorSearchSettingsUpdate>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub change_reader: Option<DesktopRuntimeChangeReaderSettingsUpdate>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub lifecycle: Option<DesktopRuntimeLifecycleSettingsUpdate>,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
501#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
502#[cfg_attr(feature = "ts", ts(export))]
503pub struct DesktopSummaryProviderDetectResponse {
504 pub detected: bool,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub provider: Option<DesktopSummaryProviderId>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub transport: Option<DesktopSummaryProviderTransport>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub model: Option<String>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub endpoint: Option<String>,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
516#[serde(rename_all = "snake_case")]
517#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
518#[cfg_attr(feature = "ts", ts(export))]
519pub enum DesktopSummaryProviderId {
520 Disabled,
521 Ollama,
522 CodexExec,
523 ClaudeCli,
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
527#[serde(rename_all = "snake_case")]
528#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
529#[cfg_attr(feature = "ts", ts(export))]
530pub enum DesktopSummaryProviderTransport {
531 None,
532 Cli,
533 Http,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
537#[serde(rename_all = "snake_case")]
538#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
539#[cfg_attr(feature = "ts", ts(export))]
540pub enum DesktopSummarySourceMode {
541 SessionOnly,
542 SessionOrGitChanges,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
546#[serde(rename_all = "snake_case")]
547#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
548#[cfg_attr(feature = "ts", ts(export))]
549pub enum DesktopSummaryResponseStyle {
550 Compact,
551 Standard,
552 Detailed,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
556#[serde(rename_all = "snake_case")]
557#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
558#[cfg_attr(feature = "ts", ts(export))]
559pub enum DesktopSummaryOutputShape {
560 Layered,
561 FileList,
562 SecurityFirst,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
566#[serde(rename_all = "snake_case")]
567#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
568#[cfg_attr(feature = "ts", ts(export))]
569pub enum DesktopSummaryTriggerMode {
570 Manual,
571 OnSessionSave,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
575#[serde(rename_all = "snake_case")]
576#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
577#[cfg_attr(feature = "ts", ts(export))]
578pub enum DesktopSummaryStorageBackend {
579 HiddenRef,
580 LocalDb,
581 None,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
585#[serde(rename_all = "snake_case")]
586#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
587#[cfg_attr(feature = "ts", ts(export))]
588pub enum DesktopSummaryBatchExecutionMode {
589 Manual,
590 OnAppStart,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
594#[serde(rename_all = "snake_case")]
595#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
596#[cfg_attr(feature = "ts", ts(export))]
597pub enum DesktopSummaryBatchScope {
598 RecentDays,
599 All,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
603#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
604#[cfg_attr(feature = "ts", ts(export))]
605pub struct DesktopRuntimeSummaryProviderSettings {
606 pub id: DesktopSummaryProviderId,
607 pub transport: DesktopSummaryProviderTransport,
608 pub endpoint: String,
609 pub model: String,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize)]
613#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
614#[cfg_attr(feature = "ts", ts(export))]
615pub struct DesktopRuntimeSummaryPromptSettings {
616 pub template: String,
617 pub default_template: String,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
621#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
622#[cfg_attr(feature = "ts", ts(export))]
623pub struct DesktopRuntimeSummaryResponseSettings {
624 pub style: DesktopSummaryResponseStyle,
625 pub shape: DesktopSummaryOutputShape,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
629#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
630#[cfg_attr(feature = "ts", ts(export))]
631pub struct DesktopRuntimeSummaryStorageSettings {
632 pub trigger: DesktopSummaryTriggerMode,
633 pub backend: DesktopSummaryStorageBackend,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
637#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
638#[cfg_attr(feature = "ts", ts(export))]
639pub struct DesktopRuntimeSummaryBatchSettings {
640 pub execution_mode: DesktopSummaryBatchExecutionMode,
641 pub scope: DesktopSummaryBatchScope,
642 pub recent_days: u16,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize)]
646#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
647#[cfg_attr(feature = "ts", ts(export))]
648pub struct DesktopRuntimeSummarySettings {
649 pub provider: DesktopRuntimeSummaryProviderSettings,
650 pub prompt: DesktopRuntimeSummaryPromptSettings,
651 pub response: DesktopRuntimeSummaryResponseSettings,
652 pub storage: DesktopRuntimeSummaryStorageSettings,
653 pub source_mode: DesktopSummarySourceMode,
654 pub batch: DesktopRuntimeSummaryBatchSettings,
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize)]
658#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
659#[cfg_attr(feature = "ts", ts(export))]
660pub struct DesktopRuntimeSummaryProviderSettingsUpdate {
661 pub id: DesktopSummaryProviderId,
662 pub endpoint: String,
663 pub model: String,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize)]
667#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
668#[cfg_attr(feature = "ts", ts(export))]
669pub struct DesktopRuntimeSummaryPromptSettingsUpdate {
670 pub template: String,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
674#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
675#[cfg_attr(feature = "ts", ts(export))]
676pub struct DesktopRuntimeSummaryResponseSettingsUpdate {
677 pub style: DesktopSummaryResponseStyle,
678 pub shape: DesktopSummaryOutputShape,
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
682#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
683#[cfg_attr(feature = "ts", ts(export))]
684pub struct DesktopRuntimeSummaryStorageSettingsUpdate {
685 pub trigger: DesktopSummaryTriggerMode,
686 pub backend: DesktopSummaryStorageBackend,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
690#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
691#[cfg_attr(feature = "ts", ts(export))]
692pub struct DesktopRuntimeSummaryBatchSettingsUpdate {
693 pub execution_mode: DesktopSummaryBatchExecutionMode,
694 pub scope: DesktopSummaryBatchScope,
695 pub recent_days: u16,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
699#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
700#[cfg_attr(feature = "ts", ts(export))]
701pub struct DesktopRuntimeSummarySettingsUpdate {
702 pub provider: DesktopRuntimeSummaryProviderSettingsUpdate,
703 pub prompt: DesktopRuntimeSummaryPromptSettingsUpdate,
704 pub response: DesktopRuntimeSummaryResponseSettingsUpdate,
705 pub storage: DesktopRuntimeSummaryStorageSettingsUpdate,
706 pub source_mode: DesktopSummarySourceMode,
707 pub batch: DesktopRuntimeSummaryBatchSettingsUpdate,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
711#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
712#[cfg_attr(feature = "ts", ts(export))]
713pub struct DesktopRuntimeSummaryUiConstraints {
714 pub source_mode_locked: bool,
715 pub source_mode_locked_value: DesktopSummarySourceMode,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
719#[serde(rename_all = "snake_case")]
720#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
721#[cfg_attr(feature = "ts", ts(export))]
722pub enum DesktopVectorSearchProvider {
723 Ollama,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
727#[serde(rename_all = "snake_case")]
728#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
729#[cfg_attr(feature = "ts", ts(export))]
730pub enum DesktopVectorSearchGranularity {
731 EventLineChunk,
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
735#[serde(rename_all = "snake_case")]
736#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
737#[cfg_attr(feature = "ts", ts(export))]
738pub enum DesktopVectorChunkingMode {
739 Auto,
740 Manual,
741}
742
743#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
744#[serde(rename_all = "snake_case")]
745#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
746#[cfg_attr(feature = "ts", ts(export))]
747pub enum DesktopVectorInstallState {
748 NotInstalled,
749 Installing,
750 Ready,
751 Failed,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
755#[serde(rename_all = "snake_case")]
756#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
757#[cfg_attr(feature = "ts", ts(export))]
758pub enum DesktopVectorIndexState {
759 Idle,
760 Running,
761 Complete,
762 Failed,
763}
764
765#[derive(Debug, Clone, Serialize, Deserialize)]
766#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
767#[cfg_attr(feature = "ts", ts(export))]
768pub struct DesktopRuntimeVectorSearchSettings {
769 pub enabled: bool,
770 pub provider: DesktopVectorSearchProvider,
771 pub model: String,
772 pub endpoint: String,
773 pub granularity: DesktopVectorSearchGranularity,
774 pub chunking_mode: DesktopVectorChunkingMode,
775 pub chunk_size_lines: u16,
776 pub chunk_overlap_lines: u16,
777 pub top_k_chunks: u16,
778 pub top_k_sessions: u16,
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
782#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
783#[cfg_attr(feature = "ts", ts(export))]
784pub struct DesktopRuntimeVectorSearchSettingsUpdate {
785 pub enabled: bool,
786 pub provider: DesktopVectorSearchProvider,
787 pub model: String,
788 pub endpoint: String,
789 pub granularity: DesktopVectorSearchGranularity,
790 pub chunking_mode: DesktopVectorChunkingMode,
791 pub chunk_size_lines: u16,
792 pub chunk_overlap_lines: u16,
793 pub top_k_chunks: u16,
794 pub top_k_sessions: u16,
795}
796
797#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
798#[serde(rename_all = "snake_case")]
799#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
800#[cfg_attr(feature = "ts", ts(export))]
801pub enum DesktopChangeReaderScope {
802 SummaryOnly,
803 FullContext,
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
807#[serde(rename_all = "snake_case")]
808#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
809#[cfg_attr(feature = "ts", ts(export))]
810pub enum DesktopChangeReaderVoiceProvider {
811 Openai,
812}
813
814#[derive(Debug, Clone, Serialize, Deserialize)]
815#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
816#[cfg_attr(feature = "ts", ts(export))]
817pub struct DesktopRuntimeChangeReaderVoiceSettings {
818 pub enabled: bool,
819 pub provider: DesktopChangeReaderVoiceProvider,
820 pub model: String,
821 pub voice: String,
822 pub api_key_configured: bool,
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize)]
826#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
827#[cfg_attr(feature = "ts", ts(export))]
828pub struct DesktopRuntimeChangeReaderVoiceSettingsUpdate {
829 pub enabled: bool,
830 pub provider: DesktopChangeReaderVoiceProvider,
831 pub model: String,
832 pub voice: String,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
834 pub api_key: Option<String>,
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize)]
838#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
839#[cfg_attr(feature = "ts", ts(export))]
840pub struct DesktopRuntimeChangeReaderSettings {
841 pub enabled: bool,
842 pub scope: DesktopChangeReaderScope,
843 pub qa_enabled: bool,
844 pub max_context_chars: u32,
845 pub voice: DesktopRuntimeChangeReaderVoiceSettings,
846}
847
848#[derive(Debug, Clone, Serialize, Deserialize)]
849#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
850#[cfg_attr(feature = "ts", ts(export))]
851pub struct DesktopRuntimeChangeReaderSettingsUpdate {
852 pub enabled: bool,
853 pub scope: DesktopChangeReaderScope,
854 pub qa_enabled: bool,
855 pub max_context_chars: u32,
856 pub voice: DesktopRuntimeChangeReaderVoiceSettingsUpdate,
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize)]
860#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
861#[cfg_attr(feature = "ts", ts(export))]
862pub struct DesktopRuntimeLifecycleSettings {
863 pub enabled: bool,
864 pub session_ttl_days: u32,
865 pub summary_ttl_days: u32,
866 pub cleanup_interval_secs: u64,
867}
868
869#[derive(Debug, Clone, Serialize, Deserialize)]
870#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
871#[cfg_attr(feature = "ts", ts(export))]
872pub struct DesktopRuntimeLifecycleSettingsUpdate {
873 pub enabled: bool,
874 pub session_ttl_days: u32,
875 pub summary_ttl_days: u32,
876 pub cleanup_interval_secs: u64,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize)]
880#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
881#[cfg_attr(feature = "ts", ts(export))]
882pub struct DesktopVectorPreflightResponse {
883 pub provider: DesktopVectorSearchProvider,
884 pub endpoint: String,
885 pub model: String,
886 pub ollama_reachable: bool,
887 pub model_installed: bool,
888 pub install_state: DesktopVectorInstallState,
889 pub progress_pct: u8,
890 #[serde(default, skip_serializing_if = "Option::is_none")]
891 pub message: Option<String>,
892}
893
894#[derive(Debug, Clone, Serialize, Deserialize)]
895#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
896#[cfg_attr(feature = "ts", ts(export))]
897pub struct DesktopVectorInstallStatusResponse {
898 pub state: DesktopVectorInstallState,
899 pub model: String,
900 pub progress_pct: u8,
901 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub message: Option<String>,
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize)]
906#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
907#[cfg_attr(feature = "ts", ts(export))]
908pub struct DesktopVectorIndexStatusResponse {
909 pub state: DesktopVectorIndexState,
910 pub processed_sessions: u32,
911 pub total_sessions: u32,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub message: Option<String>,
914 #[serde(default, skip_serializing_if = "Option::is_none")]
915 pub started_at: Option<String>,
916 #[serde(default, skip_serializing_if = "Option::is_none")]
917 pub finished_at: Option<String>,
918}
919
920#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
921#[serde(rename_all = "snake_case")]
922#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
923#[cfg_attr(feature = "ts", ts(export))]
924pub enum DesktopSummaryBatchState {
925 Idle,
926 Running,
927 Complete,
928 Failed,
929}
930
931#[derive(Debug, Clone, Serialize, Deserialize)]
932#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
933#[cfg_attr(feature = "ts", ts(export))]
934pub struct DesktopSummaryBatchStatusResponse {
935 pub state: DesktopSummaryBatchState,
936 pub processed_sessions: u32,
937 pub total_sessions: u32,
938 pub failed_sessions: u32,
939 #[serde(default, skip_serializing_if = "Option::is_none")]
940 pub message: Option<String>,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
942 pub started_at: Option<String>,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
944 pub finished_at: Option<String>,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
948#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
949#[cfg_attr(feature = "ts", ts(export))]
950pub struct DesktopVectorSessionMatch {
951 pub session: SessionSummary,
952 pub score: f32,
953 pub chunk_id: String,
954 pub start_line: u32,
955 pub end_line: u32,
956 pub snippet: String,
957}
958
959#[derive(Debug, Clone, Serialize, Deserialize)]
960#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
961#[cfg_attr(feature = "ts", ts(export))]
962pub struct DesktopVectorSearchResponse {
963 pub query: String,
964 #[serde(default)]
965 pub sessions: Vec<DesktopVectorSessionMatch>,
966 #[serde(default, skip_serializing_if = "Option::is_none")]
967 pub next_cursor: Option<String>,
968 pub total_candidates: u32,
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize)]
973#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
974#[cfg_attr(feature = "ts", ts(export))]
975pub struct DesktopSessionSummaryResponse {
976 pub session_id: String,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
978 #[cfg_attr(feature = "ts", ts(type = "any"))]
979 pub summary: Option<serde_json::Value>,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
981 #[cfg_attr(feature = "ts", ts(type = "any"))]
982 pub source_details: Option<serde_json::Value>,
983 #[serde(default)]
984 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
985 pub diff_tree: Vec<serde_json::Value>,
986 #[serde(default, skip_serializing_if = "Option::is_none")]
987 pub source_kind: Option<String>,
988 #[serde(default, skip_serializing_if = "Option::is_none")]
989 pub generation_kind: Option<String>,
990 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub error: Option<String>,
992}
993
994#[derive(Debug, Clone, Serialize, Deserialize)]
995#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
996#[cfg_attr(feature = "ts", ts(export))]
997pub struct DesktopChangeReadRequest {
998 pub session_id: String,
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1000 pub scope: Option<DesktopChangeReaderScope>,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1005#[cfg_attr(feature = "ts", ts(export))]
1006pub struct DesktopChangeReadResponse {
1007 pub session_id: String,
1008 pub scope: DesktopChangeReaderScope,
1009 pub narrative: String,
1010 #[serde(default)]
1011 pub citations: Vec<String>,
1012 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub provider: Option<DesktopSummaryProviderId>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1015 pub warning: Option<String>,
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1019#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1020#[cfg_attr(feature = "ts", ts(export))]
1021pub struct DesktopChangeQuestionRequest {
1022 pub session_id: String,
1023 pub question: String,
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1025 pub scope: Option<DesktopChangeReaderScope>,
1026}
1027
1028#[derive(Debug, Clone, Serialize, Deserialize)]
1029#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1030#[cfg_attr(feature = "ts", ts(export))]
1031pub struct DesktopChangeReaderTtsRequest {
1032 pub text: String,
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1034 pub session_id: Option<String>,
1035 #[serde(default, skip_serializing_if = "Option::is_none")]
1036 pub scope: Option<DesktopChangeReaderScope>,
1037}
1038
1039#[derive(Debug, Clone, Serialize, Deserialize)]
1040#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1041#[cfg_attr(feature = "ts", ts(export))]
1042pub struct DesktopChangeReaderTtsResponse {
1043 pub mime_type: String,
1044 pub audio_base64: String,
1045 #[serde(default, skip_serializing_if = "Option::is_none")]
1046 pub warning: Option<String>,
1047}
1048
1049#[derive(Debug, Clone, Serialize, Deserialize)]
1050#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1051#[cfg_attr(feature = "ts", ts(export))]
1052pub struct DesktopChangeQuestionResponse {
1053 pub session_id: String,
1054 pub question: String,
1055 pub scope: DesktopChangeReaderScope,
1056 pub answer: String,
1057 #[serde(default)]
1058 pub citations: Vec<String>,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1060 pub provider: Option<DesktopSummaryProviderId>,
1061 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub warning: Option<String>,
1063}
1064
1065#[derive(Debug, Clone, Serialize, Deserialize)]
1067#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1068#[cfg_attr(feature = "ts", ts(export))]
1069pub struct DesktopApiError {
1070 pub code: String,
1071 pub status: u16,
1072 pub message: String,
1073 #[serde(default, skip_serializing_if = "Option::is_none")]
1074 #[cfg_attr(feature = "ts", ts(type = "Record<string, any> | null"))]
1075 pub details: Option<serde_json::Value>,
1076}
1077
1078impl SessionListQuery {
1079 pub fn is_public_feed_cacheable(
1081 &self,
1082 has_auth_header: bool,
1083 has_session_cookie: bool,
1084 ) -> bool {
1085 !has_auth_header
1086 && !has_session_cookie
1087 && self.search.as_deref().is_none_or(|s| s.trim().is_empty())
1088 && self
1089 .git_repo_name
1090 .as_deref()
1091 .is_none_or(|repo| repo.trim().is_empty())
1092 && self.page <= 10
1093 && self.per_page <= 50
1094 }
1095}
1096
1097#[cfg(test)]
1098mod session_list_query_tests {
1099 use super::*;
1100
1101 fn base_query() -> SessionListQuery {
1102 SessionListQuery {
1103 page: 1,
1104 per_page: 20,
1105 search: None,
1106 tool: None,
1107 git_repo_name: None,
1108 sort: None,
1109 time_range: None,
1110 }
1111 }
1112
1113 #[test]
1114 fn public_feed_cacheable_when_anonymous_default_feed() {
1115 let q = base_query();
1116 assert!(q.is_public_feed_cacheable(false, false));
1117 }
1118
1119 #[test]
1120 fn public_feed_not_cacheable_with_auth_or_cookie() {
1121 let q = base_query();
1122 assert!(!q.is_public_feed_cacheable(true, false));
1123 assert!(!q.is_public_feed_cacheable(false, true));
1124 }
1125
1126 #[test]
1127 fn public_feed_not_cacheable_for_search_or_large_page() {
1128 let mut q = base_query();
1129 q.search = Some("hello".into());
1130 assert!(!q.is_public_feed_cacheable(false, false));
1131
1132 let mut q = base_query();
1133 q.git_repo_name = Some("org/repo".into());
1134 assert!(!q.is_public_feed_cacheable(false, false));
1135
1136 let mut q = base_query();
1137 q.page = 11;
1138 assert!(!q.is_public_feed_cacheable(false, false));
1139
1140 let mut q = base_query();
1141 q.per_page = 100;
1142 assert!(!q.is_public_feed_cacheable(false, false));
1143 }
1144}
1145
1146fn default_page() -> u32 {
1147 1
1148}
1149fn default_per_page() -> u32 {
1150 20
1151}
1152fn default_max_active_agents() -> i64 {
1153 1
1154}
1155
1156fn default_score_plugin() -> String {
1157 opensession_core::scoring::DEFAULT_SCORE_PLUGIN.to_string()
1158}
1159
1160#[derive(Debug, Serialize, Deserialize)]
1162#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1163#[cfg_attr(feature = "ts", ts(export))]
1164pub struct SessionDetail {
1165 #[serde(flatten)]
1166 #[cfg_attr(feature = "ts", ts(flatten))]
1167 pub summary: SessionSummary,
1168 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1169 pub linked_sessions: Vec<SessionLink>,
1170}
1171
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1174#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1175#[cfg_attr(feature = "ts", ts(export))]
1176pub struct SessionLink {
1177 pub session_id: String,
1178 pub linked_session_id: String,
1179 pub link_type: LinkType,
1180 pub created_at: String,
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1185#[serde(tag = "kind", rename_all = "snake_case")]
1186#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1187#[cfg_attr(feature = "ts", ts(export))]
1188pub enum ParseSource {
1189 Git {
1191 remote: String,
1192 r#ref: String,
1193 path: String,
1194 },
1195 Github {
1197 owner: String,
1198 repo: String,
1199 r#ref: String,
1200 path: String,
1201 },
1202 Inline {
1204 filename: String,
1205 content_base64: String,
1207 },
1208}
1209
1210#[derive(Debug, Clone, Serialize, Deserialize)]
1212#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1213#[cfg_attr(feature = "ts", ts(export))]
1214pub struct ParseCandidate {
1215 pub id: String,
1216 pub confidence: u8,
1217 pub reason: String,
1218}
1219
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1222#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1223#[cfg_attr(feature = "ts", ts(export))]
1224pub struct ParsePreviewRequest {
1225 pub source: ParseSource,
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1227 pub parser_hint: Option<String>,
1228}
1229
1230#[derive(Debug, Clone, Serialize, Deserialize)]
1232#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1233#[cfg_attr(feature = "ts", ts(export))]
1234pub struct ParsePreviewResponse {
1235 pub parser_used: String,
1236 #[serde(default)]
1237 pub parser_candidates: Vec<ParseCandidate>,
1238 #[cfg_attr(feature = "ts", ts(type = "any"))]
1239 pub session: Session,
1240 pub source: ParseSource,
1241 #[serde(default)]
1242 pub warnings: Vec<String>,
1243 #[serde(default, skip_serializing_if = "Option::is_none")]
1244 pub native_adapter: Option<String>,
1245}
1246
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1249#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1250#[cfg_attr(feature = "ts", ts(export))]
1251pub struct ParsePreviewErrorResponse {
1252 pub code: String,
1253 pub message: String,
1254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1255 pub parser_candidates: Vec<ParseCandidate>,
1256}
1257
1258#[derive(Debug, Clone, Serialize, Deserialize)]
1260#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1261#[cfg_attr(feature = "ts", ts(export))]
1262pub struct LocalReviewBundle {
1263 pub review_id: String,
1264 pub generated_at: String,
1265 pub pr: LocalReviewPrMeta,
1266 #[serde(default)]
1267 pub commits: Vec<LocalReviewCommit>,
1268 #[serde(default)]
1269 pub sessions: Vec<LocalReviewSession>,
1270}
1271
1272#[derive(Debug, Clone, Serialize, Deserialize)]
1274#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1275#[cfg_attr(feature = "ts", ts(export))]
1276pub struct LocalReviewPrMeta {
1277 pub url: String,
1278 pub owner: String,
1279 pub repo: String,
1280 pub number: u64,
1281 pub remote: String,
1282 pub base_sha: String,
1283 pub head_sha: String,
1284}
1285
1286#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1288#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1289#[cfg_attr(feature = "ts", ts(export))]
1290pub struct LocalReviewReviewerQa {
1291 pub question: String,
1292 #[serde(default, skip_serializing_if = "Option::is_none")]
1293 pub answer: Option<String>,
1294}
1295
1296#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1298#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1299#[cfg_attr(feature = "ts", ts(export))]
1300pub struct LocalReviewReviewerDigest {
1301 #[serde(default)]
1302 pub qa: Vec<LocalReviewReviewerQa>,
1303 #[serde(default)]
1304 pub modified_files: Vec<String>,
1305 #[serde(default)]
1306 pub test_files: Vec<String>,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1311#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1312#[cfg_attr(feature = "ts", ts(export))]
1313pub struct LocalReviewCommit {
1314 pub sha: String,
1315 pub title: String,
1316 pub author_name: String,
1317 pub author_email: String,
1318 pub authored_at: String,
1319 #[serde(default)]
1320 pub session_ids: Vec<String>,
1321 #[serde(default)]
1322 pub reviewer_digest: LocalReviewReviewerDigest,
1323 #[serde(default, skip_serializing_if = "Option::is_none")]
1324 pub semantic_summary: Option<LocalReviewSemanticSummary>,
1325}
1326
1327#[derive(Debug, Clone, Serialize, Deserialize)]
1329#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1330#[cfg_attr(feature = "ts", ts(export))]
1331pub struct LocalReviewLayerFileChange {
1332 pub layer: String,
1333 pub summary: String,
1334 #[serde(default)]
1335 pub files: Vec<String>,
1336}
1337
1338#[derive(Debug, Clone, Serialize, Deserialize)]
1340#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1341#[cfg_attr(feature = "ts", ts(export))]
1342pub struct LocalReviewSemanticSummary {
1343 pub changes: String,
1344 pub auth_security: String,
1345 #[serde(default)]
1346 pub layer_file_changes: Vec<LocalReviewLayerFileChange>,
1347 pub source_kind: String,
1348 pub generation_kind: String,
1349 pub provider: String,
1350 #[serde(default, skip_serializing_if = "Option::is_none")]
1351 pub model: Option<String>,
1352 #[serde(default, skip_serializing_if = "Option::is_none")]
1353 pub error: Option<String>,
1354 #[serde(default)]
1355 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
1356 pub diff_tree: Vec<serde_json::Value>,
1357}
1358
1359#[derive(Debug, Clone, Serialize, Deserialize)]
1361#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1362#[cfg_attr(feature = "ts", ts(export))]
1363pub struct LocalReviewSession {
1364 pub session_id: String,
1365 pub ledger_ref: String,
1366 pub hail_path: String,
1367 #[serde(default)]
1368 pub commit_shas: Vec<String>,
1369 #[cfg_attr(feature = "ts", ts(type = "any"))]
1370 pub session: Session,
1371}
1372
1373#[derive(Debug, Deserialize)]
1377#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1378#[cfg_attr(feature = "ts", ts(export))]
1379pub struct StreamEventsRequest {
1380 #[cfg_attr(feature = "ts", ts(type = "any"))]
1381 pub agent: Option<Agent>,
1382 #[cfg_attr(feature = "ts", ts(type = "any"))]
1383 pub context: Option<SessionContext>,
1384 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
1385 pub events: Vec<Event>,
1386}
1387
1388#[derive(Debug, Serialize, Deserialize)]
1390#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1391#[cfg_attr(feature = "ts", ts(export))]
1392pub struct StreamEventsResponse {
1393 pub accepted: usize,
1394}
1395
1396#[derive(Debug, Serialize, Deserialize)]
1400#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1401#[cfg_attr(feature = "ts", ts(export))]
1402pub struct HealthResponse {
1403 pub status: String,
1404 pub version: String,
1405}
1406
1407#[derive(Debug, Serialize, Deserialize)]
1409#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1410#[cfg_attr(feature = "ts", ts(export))]
1411pub struct CapabilitiesResponse {
1412 pub auth_enabled: bool,
1413 pub parse_preview_enabled: bool,
1414 pub register_targets: Vec<String>,
1415 pub share_modes: Vec<String>,
1416}
1417
1418pub const DEFAULT_REGISTER_TARGETS: &[&str] = &["local", "git"];
1419pub const DEFAULT_SHARE_MODES: &[&str] = &["web", "git", "quick", "json"];
1420
1421impl CapabilitiesResponse {
1422 pub fn for_runtime(auth_enabled: bool, parse_preview_enabled: bool) -> Self {
1424 Self {
1425 auth_enabled,
1426 parse_preview_enabled,
1427 register_targets: DEFAULT_REGISTER_TARGETS
1428 .iter()
1429 .map(|target| (*target).to_string())
1430 .collect(),
1431 share_modes: DEFAULT_SHARE_MODES
1432 .iter()
1433 .map(|mode| (*mode).to_string())
1434 .collect(),
1435 }
1436 }
1437}
1438
1439#[derive(Debug, Clone)]
1446#[non_exhaustive]
1447pub enum ServiceError {
1448 BadRequest(String),
1449 Unauthorized(String),
1450 Forbidden(String),
1451 NotFound(String),
1452 Conflict(String),
1453 Internal(String),
1454}
1455
1456impl ServiceError {
1457 pub fn status_code(&self) -> u16 {
1459 match self {
1460 Self::BadRequest(_) => 400,
1461 Self::Unauthorized(_) => 401,
1462 Self::Forbidden(_) => 403,
1463 Self::NotFound(_) => 404,
1464 Self::Conflict(_) => 409,
1465 Self::Internal(_) => 500,
1466 }
1467 }
1468
1469 pub fn code(&self) -> &'static str {
1471 match self {
1472 Self::BadRequest(_) => "bad_request",
1473 Self::Unauthorized(_) => "unauthorized",
1474 Self::Forbidden(_) => "forbidden",
1475 Self::NotFound(_) => "not_found",
1476 Self::Conflict(_) => "conflict",
1477 Self::Internal(_) => "internal",
1478 }
1479 }
1480
1481 pub fn message(&self) -> &str {
1483 match self {
1484 Self::BadRequest(m)
1485 | Self::Unauthorized(m)
1486 | Self::Forbidden(m)
1487 | Self::NotFound(m)
1488 | Self::Conflict(m)
1489 | Self::Internal(m) => m,
1490 }
1491 }
1492
1493 pub fn from_db<E: std::fmt::Display>(context: &str) -> impl FnOnce(E) -> Self + '_ {
1495 move |e| Self::Internal(format!("{context}: {e}"))
1496 }
1497}
1498
1499impl std::fmt::Display for ServiceError {
1500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1501 write!(f, "{}", self.message())
1502 }
1503}
1504
1505impl std::error::Error for ServiceError {}
1506
1507#[derive(Debug, Serialize)]
1511#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1512#[cfg_attr(feature = "ts", ts(export))]
1513pub struct ApiError {
1514 pub code: String,
1515 pub message: String,
1516}
1517
1518impl From<&ServiceError> for ApiError {
1519 fn from(e: &ServiceError) -> Self {
1520 Self {
1521 code: e.code().to_string(),
1522 message: e.message().to_string(),
1523 }
1524 }
1525}
1526
1527#[cfg(test)]
1530mod schema_tests {
1531 use super::*;
1532
1533 #[test]
1534 fn parse_preview_request_round_trip_git() {
1535 let req = ParsePreviewRequest {
1536 source: ParseSource::Git {
1537 remote: "https://github.com/hwisu/opensession".to_string(),
1538 r#ref: "main".to_string(),
1539 path: "sessions/demo.hail.jsonl".to_string(),
1540 },
1541 parser_hint: Some("hail".to_string()),
1542 };
1543
1544 let json = serde_json::to_string(&req).expect("request should serialize");
1545 let decoded: ParsePreviewRequest =
1546 serde_json::from_str(&json).expect("request should deserialize");
1547
1548 match decoded.source {
1549 ParseSource::Git {
1550 remote,
1551 r#ref,
1552 path,
1553 } => {
1554 assert_eq!(remote, "https://github.com/hwisu/opensession");
1555 assert_eq!(r#ref, "main");
1556 assert_eq!(path, "sessions/demo.hail.jsonl");
1557 }
1558 _ => panic!("expected git parse source"),
1559 }
1560 assert_eq!(decoded.parser_hint.as_deref(), Some("hail"));
1561 }
1562
1563 #[test]
1564 fn parse_preview_request_round_trip_github_compat() {
1565 let req = ParsePreviewRequest {
1566 source: ParseSource::Github {
1567 owner: "hwisu".to_string(),
1568 repo: "opensession".to_string(),
1569 r#ref: "main".to_string(),
1570 path: "sessions/demo.hail.jsonl".to_string(),
1571 },
1572 parser_hint: Some("hail".to_string()),
1573 };
1574
1575 let json = serde_json::to_string(&req).expect("request should serialize");
1576 let decoded: ParsePreviewRequest =
1577 serde_json::from_str(&json).expect("request should deserialize");
1578
1579 match decoded.source {
1580 ParseSource::Github {
1581 owner,
1582 repo,
1583 r#ref,
1584 path,
1585 } => {
1586 assert_eq!(owner, "hwisu");
1587 assert_eq!(repo, "opensession");
1588 assert_eq!(r#ref, "main");
1589 assert_eq!(path, "sessions/demo.hail.jsonl");
1590 }
1591 _ => panic!("expected github parse source"),
1592 }
1593 assert_eq!(decoded.parser_hint.as_deref(), Some("hail"));
1594 }
1595
1596 #[test]
1597 fn parse_preview_error_response_round_trip_with_candidates() {
1598 let payload = ParsePreviewErrorResponse {
1599 code: "parser_selection_required".to_string(),
1600 message: "choose parser".to_string(),
1601 parser_candidates: vec![ParseCandidate {
1602 id: "codex".to_string(),
1603 confidence: 89,
1604 reason: "event markers".to_string(),
1605 }],
1606 };
1607
1608 let json = serde_json::to_string(&payload).expect("error payload should serialize");
1609 let decoded: ParsePreviewErrorResponse =
1610 serde_json::from_str(&json).expect("error payload should deserialize");
1611
1612 assert_eq!(decoded.code, "parser_selection_required");
1613 assert_eq!(decoded.parser_candidates.len(), 1);
1614 assert_eq!(decoded.parser_candidates[0].id, "codex");
1615 }
1616
1617 #[test]
1618 fn local_review_bundle_round_trip() {
1619 let mut sample_session = Session::new(
1620 "s-review-1".to_string(),
1621 Agent {
1622 provider: "openai".to_string(),
1623 model: "gpt-5".to_string(),
1624 tool: "codex".to_string(),
1625 tool_version: None,
1626 },
1627 );
1628 sample_session.recompute_stats();
1629
1630 let payload = LocalReviewBundle {
1631 review_id: "gh-org-repo-pr1-abc1234".to_string(),
1632 generated_at: "2026-02-24T00:00:00Z".to_string(),
1633 pr: LocalReviewPrMeta {
1634 url: "https://github.com/org/repo/pull/1".to_string(),
1635 owner: "org".to_string(),
1636 repo: "repo".to_string(),
1637 number: 1,
1638 remote: "origin".to_string(),
1639 base_sha: "a".repeat(40),
1640 head_sha: "b".repeat(40),
1641 },
1642 commits: vec![LocalReviewCommit {
1643 sha: "c".repeat(40),
1644 title: "feat: add review flow".to_string(),
1645 author_name: "Alice".to_string(),
1646 author_email: "alice@example.com".to_string(),
1647 authored_at: "2026-02-24T00:00:00Z".to_string(),
1648 session_ids: vec!["s-review-1".to_string()],
1649 reviewer_digest: LocalReviewReviewerDigest {
1650 qa: vec![LocalReviewReviewerQa {
1651 question: "Which route should we verify first?".to_string(),
1652 answer: Some("Start with /review/local/:id live path.".to_string()),
1653 }],
1654 modified_files: vec![
1655 "crates/cli/src/review.rs".to_string(),
1656 "web/src/routes/review/local/[id]/+page.svelte".to_string(),
1657 ],
1658 test_files: vec!["web/e2e-live/live-review-local.spec.ts".to_string()],
1659 },
1660 semantic_summary: Some(LocalReviewSemanticSummary {
1661 changes: "Updated review flow wiring".to_string(),
1662 auth_security: "none detected".to_string(),
1663 layer_file_changes: vec![LocalReviewLayerFileChange {
1664 layer: "application".to_string(),
1665 summary: "Added bundle resolver".to_string(),
1666 files: vec!["crates/cli/src/review.rs".to_string()],
1667 }],
1668 source_kind: "git_commit".to_string(),
1669 generation_kind: "heuristic_fallback".to_string(),
1670 provider: "disabled".to_string(),
1671 model: None,
1672 error: None,
1673 diff_tree: Vec::new(),
1674 }),
1675 }],
1676 sessions: vec![LocalReviewSession {
1677 session_id: "s-review-1".to_string(),
1678 ledger_ref: "refs/remotes/origin/opensession/branches/bWFpbg".to_string(),
1679 hail_path: "v1/se/s-review-1.hail.jsonl".to_string(),
1680 commit_shas: vec!["c".repeat(40)],
1681 session: sample_session,
1682 }],
1683 };
1684
1685 let json = serde_json::to_string(&payload).expect("review bundle should serialize");
1686 let decoded: LocalReviewBundle =
1687 serde_json::from_str(&json).expect("review bundle should deserialize");
1688
1689 assert_eq!(decoded.review_id, "gh-org-repo-pr1-abc1234");
1690 assert_eq!(decoded.pr.number, 1);
1691 assert_eq!(decoded.commits.len(), 1);
1692 assert_eq!(decoded.sessions.len(), 1);
1693 assert_eq!(decoded.sessions[0].session_id, "s-review-1");
1694 assert_eq!(
1695 decoded.commits[0]
1696 .reviewer_digest
1697 .qa
1698 .first()
1699 .map(|row| row.question.as_str()),
1700 Some("Which route should we verify first?")
1701 );
1702 assert_eq!(decoded.commits[0].reviewer_digest.test_files.len(), 1);
1703 }
1704
1705 #[test]
1706 fn capabilities_response_round_trip_includes_new_fields() {
1707 let caps = CapabilitiesResponse::for_runtime(true, true);
1708
1709 let json = serde_json::to_string(&caps).expect("capabilities should serialize");
1710 let decoded: CapabilitiesResponse =
1711 serde_json::from_str(&json).expect("capabilities should deserialize");
1712
1713 assert!(decoded.auth_enabled);
1714 assert!(decoded.parse_preview_enabled);
1715 assert_eq!(decoded.register_targets, vec!["local", "git"]);
1716 assert_eq!(decoded.share_modes, vec!["web", "git", "quick", "json"]);
1717 }
1718
1719 #[test]
1720 fn capabilities_defaults_are_stable() {
1721 assert_eq!(DEFAULT_REGISTER_TARGETS, &["local", "git"]);
1722 assert_eq!(DEFAULT_SHARE_MODES, &["web", "git", "quick", "json"]);
1723 }
1724}
1725
1726#[cfg(all(test, feature = "ts"))]
1727mod tests {
1728 use super::*;
1729 use std::io::Write;
1730 use std::path::PathBuf;
1731 use ts_rs::TS;
1732
1733 #[test]
1735 fn export_typescript() {
1736 let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1737 .join("../../packages/ui/src/api-types.generated.ts");
1738
1739 let cfg = ts_rs::Config::new().with_large_int("number");
1740 let mut parts: Vec<String> = Vec::new();
1741 parts.push("// AUTO-GENERATED by opensession-api — DO NOT EDIT".to_string());
1742 parts.push(
1743 "// Regenerate with: cargo test -p opensession-api -- export_typescript".to_string(),
1744 );
1745 parts.push(String::new());
1746
1747 macro_rules! collect_ts {
1751 ($($t:ty),+ $(,)?) => {
1752 $(
1753 let decl = <$t>::decl(&cfg);
1754 let is_struct_decl = decl.contains(" = {") && !decl.contains("} |");
1755 let decl = if is_struct_decl {
1756 decl
1758 .replacen("type ", "export interface ", 1)
1759 .replace(" = {", " {")
1760 .trim_end_matches(';')
1761 .to_string()
1762 } else {
1763 decl
1765 .replacen("type ", "export type ", 1)
1766 .trim_end_matches(';')
1767 .to_string()
1768 };
1769 parts.push(decl);
1770 parts.push(String::new());
1771 )+
1772 };
1773 }
1774
1775 collect_ts!(
1776 SortOrder,
1778 TimeRange,
1779 LinkType,
1780 AuthRegisterRequest,
1782 LoginRequest,
1783 AuthTokenResponse,
1784 RefreshRequest,
1785 LogoutRequest,
1786 ChangePasswordRequest,
1787 VerifyResponse,
1788 UserSettingsResponse,
1789 OkResponse,
1790 IssueApiKeyResponse,
1791 GitCredentialSummary,
1792 ListGitCredentialsResponse,
1793 CreateGitCredentialRequest,
1794 OAuthLinkResponse,
1795 UploadResponse,
1797 SessionSummary,
1798 SessionListResponse,
1799 SessionListQuery,
1800 DesktopSessionListQuery,
1801 SessionRepoListResponse,
1802 DesktopHandoffBuildRequest,
1803 DesktopHandoffBuildResponse,
1804 DesktopQuickShareRequest,
1805 DesktopQuickShareResponse,
1806 DesktopContractVersionResponse,
1807 DesktopSummaryProviderId,
1808 DesktopSummaryProviderTransport,
1809 DesktopSummarySourceMode,
1810 DesktopSummaryResponseStyle,
1811 DesktopSummaryOutputShape,
1812 DesktopSummaryTriggerMode,
1813 DesktopSummaryStorageBackend,
1814 DesktopSummaryBatchExecutionMode,
1815 DesktopSummaryBatchScope,
1816 DesktopRuntimeSummaryProviderSettings,
1817 DesktopRuntimeSummaryPromptSettings,
1818 DesktopRuntimeSummaryResponseSettings,
1819 DesktopRuntimeSummaryStorageSettings,
1820 DesktopRuntimeSummaryBatchSettings,
1821 DesktopRuntimeSummarySettings,
1822 DesktopRuntimeSummaryProviderSettingsUpdate,
1823 DesktopRuntimeSummaryPromptSettingsUpdate,
1824 DesktopRuntimeSummaryResponseSettingsUpdate,
1825 DesktopRuntimeSummaryStorageSettingsUpdate,
1826 DesktopRuntimeSummaryBatchSettingsUpdate,
1827 DesktopRuntimeSummarySettingsUpdate,
1828 DesktopRuntimeSummaryUiConstraints,
1829 DesktopVectorSearchProvider,
1830 DesktopVectorSearchGranularity,
1831 DesktopVectorChunkingMode,
1832 DesktopVectorInstallState,
1833 DesktopVectorIndexState,
1834 DesktopRuntimeVectorSearchSettings,
1835 DesktopRuntimeVectorSearchSettingsUpdate,
1836 DesktopChangeReaderScope,
1837 DesktopChangeReaderVoiceProvider,
1838 DesktopRuntimeChangeReaderVoiceSettings,
1839 DesktopRuntimeChangeReaderVoiceSettingsUpdate,
1840 DesktopRuntimeChangeReaderSettings,
1841 DesktopRuntimeChangeReaderSettingsUpdate,
1842 DesktopRuntimeLifecycleSettings,
1843 DesktopRuntimeLifecycleSettingsUpdate,
1844 DesktopVectorPreflightResponse,
1845 DesktopVectorInstallStatusResponse,
1846 DesktopVectorIndexStatusResponse,
1847 DesktopSummaryBatchState,
1848 DesktopSummaryBatchStatusResponse,
1849 DesktopVectorSessionMatch,
1850 DesktopVectorSearchResponse,
1851 DesktopRuntimeSettingsResponse,
1852 DesktopRuntimeSettingsUpdateRequest,
1853 DesktopSummaryProviderDetectResponse,
1854 DesktopSessionSummaryResponse,
1855 DesktopChangeReadRequest,
1856 DesktopChangeReadResponse,
1857 DesktopChangeQuestionRequest,
1858 DesktopChangeReaderTtsRequest,
1859 DesktopChangeReaderTtsResponse,
1860 DesktopChangeQuestionResponse,
1861 DesktopApiError,
1862 SessionDetail,
1863 SessionLink,
1864 ParseSource,
1865 ParseCandidate,
1866 ParsePreviewRequest,
1867 ParsePreviewResponse,
1868 ParsePreviewErrorResponse,
1869 LocalReviewBundle,
1870 LocalReviewPrMeta,
1871 LocalReviewReviewerQa,
1872 LocalReviewReviewerDigest,
1873 LocalReviewCommit,
1874 LocalReviewLayerFileChange,
1875 LocalReviewSemanticSummary,
1876 LocalReviewSession,
1877 oauth::AuthProvidersResponse,
1879 oauth::OAuthProviderInfo,
1880 oauth::LinkedProvider,
1881 HealthResponse,
1883 CapabilitiesResponse,
1884 ApiError,
1885 );
1886
1887 let content = parts.join("\n");
1888
1889 if let Some(parent) = out_dir.parent() {
1891 std::fs::create_dir_all(parent).ok();
1892 }
1893 let mut file = std::fs::File::create(&out_dir)
1894 .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_dir.display(), e));
1895 file.write_all(content.as_bytes())
1896 .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_dir.display(), e));
1897
1898 println!("Generated TypeScript types at: {}", out_dir.display());
1899 }
1900}