Skip to main content

opensession_api/
lib.rs

1//! Shared API types, crypto, and SQL builders for opensession.io
2//!
3//! This crate is the **single source of truth** for all API request/response types.
4//! TypeScript types are auto-generated via `ts-rs` and consumed by the frontend.
5//!
6//! To regenerate TypeScript types:
7//!   cargo test -p opensession-api -- export_typescript --nocapture
8
9use 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
20// Re-export core HAIL types for convenience
21pub use opensession_core::trace::{
22    Agent, Content, ContentBlock, Event, EventType, Session, SessionContext, Stats,
23};
24
25// ─── Shared Enums ────────────────────────────────────────────────────────────
26
27/// Sort order for session listings.
28#[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/// Time range filter for queries.
56#[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/// Type of link between two sessions.
89#[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
117// ─── Utilities ───────────────────────────────────────────────────────────────
118
119/// Safely convert `u64` to `i64`, saturating at `i64::MAX` instead of wrapping.
120pub fn saturating_i64(v: u64) -> i64 {
121    i64::try_from(v).unwrap_or(i64::MAX)
122}
123
124// ─── Auth ────────────────────────────────────────────────────────────────────
125
126/// Email + password registration.
127#[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/// Email + password login.
137#[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/// Returned on successful login / register / refresh.
146#[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/// Refresh token request.
158#[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/// Logout request (invalidate refresh token).
166#[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/// Change password request.
174#[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/// Returned by `POST /api/auth/verify` — confirms token validity.
183#[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/// Full user profile returned by `GET /api/auth/me`.
192#[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    /// Linked OAuth providers.
202    #[serde(default)]
203    pub oauth_providers: Vec<oauth::LinkedProvider>,
204}
205
206/// Generic success response for operations that don't return data.
207#[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/// Response for API key issuance. The key is visible only at issuance time.
215#[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/// Public metadata for a user-managed git credential.
223#[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/// Response for `GET /api/auth/git-credentials`.
238#[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/// Request for `POST /api/auth/git-credentials`.
247#[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/// Response for OAuth link initiation (redirect URL).
260#[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// ─── Sessions ────────────────────────────────────────────────────────────────
268
269/// Request body for `POST /api/sessions` — upload a recorded session.
270#[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/// Returned on successful session upload — contains the new session ID and URL.
294#[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/// Flat session summary returned by list/detail endpoints.
307/// This is NOT the full HAIL Session — it's a DB-derived summary.
308#[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    /// Comma-separated tags string
321    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/// Paginated session listing returned by `GET /api/sessions`.
359#[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
369/// Canonical desktop IPC contract version shared between Rust and TS clients.
370pub const DESKTOP_IPC_CONTRACT_VERSION: &str = "desktop-ipc-v6";
371
372/// Query parameters for `GET /api/sessions` — pagination, filtering, sorting.
373#[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    /// Sort order (default: recent)
385    pub sort: Option<SortOrder>,
386    /// Time range filter (default: all)
387    pub time_range: Option<TimeRange>,
388}
389
390/// Desktop session list query payload passed through Tauri invoke.
391#[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/// Repo list response used by server/worker/desktop adapters.
406#[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/// Desktop handoff build request payload.
414#[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/// Desktop handoff build response payload.
423#[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/// Desktop quick-share request payload.
437#[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/// Desktop quick-share response payload.
447#[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/// Desktop bridge contract/version handshake response.
462#[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/// Desktop runtime settings payload for App settings UI.
470#[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/// Desktop runtime settings update request.
483#[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/// Local summary provider detection result for desktop setup/settings.
500#[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/// Session summary payload returned by desktop runtime.
972#[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/// Structured desktop bridge error payload.
1066#[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    /// Returns true when this query targets the anonymous public feed and is safe to edge-cache.
1080    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/// Single session detail returned by `GET /api/sessions/:id`.
1161#[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/// A link between two sessions (e.g., handoff chain).
1173#[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/// Source descriptor for parser preview requests.
1184#[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    /// Fetch and parse a raw file from a generic Git remote/ref/path source.
1190    Git {
1191        remote: String,
1192        r#ref: String,
1193        path: String,
1194    },
1195    /// Fetch and parse a raw file from a public GitHub repository.
1196    Github {
1197        owner: String,
1198        repo: String,
1199        r#ref: String,
1200        path: String,
1201    },
1202    /// Parse inline file content supplied by clients (for local upload preview).
1203    Inline {
1204        filename: String,
1205        /// Base64-encoded UTF-8 text content.
1206        content_base64: String,
1207    },
1208}
1209
1210/// Candidate parser ranked by detection confidence.
1211#[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/// Request body for `POST /api/parse/preview`.
1221#[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/// Response body for `POST /api/parse/preview`.
1231#[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/// Structured parser preview error response.
1248#[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/// Local review bundle generated from a PR range.
1259#[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/// PR metadata for a local review bundle.
1273#[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/// Reviewer-focused digest extracted from mapped sessions for a commit.
1287#[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/// Reviewer-focused digest extracted from mapped sessions for a commit.
1297#[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/// Commit row in a local review bundle.
1310#[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/// Layer/file summary section for local review semantic payloads.
1328#[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/// Commit-level semantic summary used when session mappings are weak or absent.
1339#[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/// Session payload mapped into a local review bundle.
1360#[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// ─── Streaming Events ────────────────────────────────────────────────────────
1374
1375/// Request body for `POST /api/sessions/:id/events` — append live events.
1376#[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/// Returned by `POST /api/sessions/:id/events` — number of events accepted.
1389#[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// ─── Health ──────────────────────────────────────────────────────────────────
1397
1398/// Returned by `GET /api/health` — server liveness check.
1399#[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/// Returned by `GET /api/capabilities` — runtime feature availability.
1408#[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    /// Build runtime capability payload with shared defaults.
1423    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// ─── Service Error ───────────────────────────────────────────────────────────
1440
1441/// Framework-agnostic service error.
1442///
1443/// Each variant maps to an HTTP status code. Both the Axum server and
1444/// Cloudflare Worker convert this into the appropriate response type.
1445#[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    /// HTTP status code as a `u16`.
1458    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    /// Stable machine-readable error code.
1470    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    /// The error message.
1482    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    /// Build a closure that logs a DB/IO error and returns `Internal`.
1494    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// ─── Error ───────────────────────────────────────────────────────────────────
1508
1509/// API error payload.
1510#[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// ─── TypeScript generation ───────────────────────────────────────────────────
1528
1529#[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    /// Run with: cargo test -p opensession-api -- export_typescript --nocapture
1734    #[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        // Collect all type declarations.
1748        // Structs: `type X = {...}` → `export interface X {...}`
1749        // Enums/unions: `type X = "a" | "b"` → `export type X = "a" | "b"`
1750        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                        // Struct → export interface
1757                        decl
1758                            .replacen("type ", "export interface ", 1)
1759                            .replace(" = {", " {")
1760                            .trim_end_matches(';')
1761                            .to_string()
1762                    } else {
1763                        // Enum/union → export type
1764                        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            // Shared enums
1777            SortOrder,
1778            TimeRange,
1779            LinkType,
1780            // Auth
1781            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            // Sessions
1796            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
1878            oauth::AuthProvidersResponse,
1879            oauth::OAuthProviderInfo,
1880            oauth::LinkedProvider,
1881            // Health
1882            HealthResponse,
1883            CapabilitiesResponse,
1884            ApiError,
1885        );
1886
1887        let content = parts.join("\n");
1888
1889        // Write to file
1890        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}