vtcode_commons/
program_status.rs1use anyhow::{Result, ensure};
4use base64::{Engine, engine::general_purpose::STANDARD};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ProgramState {
9 Idle,
10 Working,
11 Blocked,
12 Done,
13 Error,
14 Clear,
15}
16
17impl ProgramState {
18 pub const fn as_str(self) -> &'static str {
19 match self {
20 Self::Idle => "idle",
21 Self::Working => "working",
22 Self::Blocked => "blocked",
23 Self::Done => "done",
24 Self::Error => "error",
25 Self::Clear => "clear",
26 }
27 }
28
29 pub const fn is_finished(self) -> bool {
30 matches!(self, Self::Done | Self::Error)
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum InteractionKind {
37 Permission,
38 Question,
39 Auth,
40}
41
42impl InteractionKind {
43 pub const fn as_str(self) -> &'static str {
44 match self {
45 Self::Permission => "permission",
46 Self::Question => "question",
47 Self::Auth => "auth",
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ProgramStatusUpdate {
55 Configure { enabled: bool },
56 Wait { token: u64, kind: InteractionKind },
57 Resume { token: u64 },
58 Outcome(ProgramState),
59}
60
61pub fn record_segment(kind: &str, source: &str) -> String {
63 use sha2::{Digest, Sha256};
64 let mut hash = Sha256::new();
65 hash.update(kind.as_bytes());
66 hash.update([0]);
67 hash.update(source.as_bytes());
68 let digest = hash.finalize();
69 let mut segment = String::with_capacity(31);
70 segment.push('t');
71 for byte in digest.iter().take(15) {
72 for nibble in [byte >> 4, byte & 0x0f] {
73 segment.push(char::from(if nibble < 10 { b'0' + nibble } else { b'a' + nibble - 10 }));
74 }
75 }
76 segment
77}
78
79fn valid_segment(segment: &str) -> bool {
80 (1..=32).contains(&segment.len())
81 && segment
82 .bytes()
83 .all(|byte| byte.is_ascii_alphanumeric() || b"_.+-".contains(&byte))
84}
85
86fn encode_text(text: &str, limit: usize) -> Result<String> {
87 ensure!(text.len() <= limit, "program status text exceeds byte limit");
88 ensure!(!text.chars().any(char::is_control), "program status text contains controls");
89 let text: String = text
91 .chars()
92 .filter(
93 |c| !matches!(c, '\u{061c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}'),
94 )
95 .collect();
96 Ok(STANDARD.encode(text))
97}
98
99pub fn encode_report(
102 id: &str,
103 state: ProgramState,
104 kind: Option<InteractionKind>,
105 title: &str,
106 message: &str,
107) -> Result<String> {
108 ensure!(
109 id.len() <= 128 && id.split('/').count() <= 8 && id.split('/').all(valid_segment),
110 "invalid program status id"
111 );
112 let title = encode_text(title, 192)?;
113 let message = encode_text(message, 2048)?;
114 let mut report = format!("\x1b]7501;state={}:id={id}:app=vtcode", state.as_str());
115 if state == ProgramState::Blocked
116 && let Some(kind) = kind
117 {
118 report.push_str(":kind=");
119 report.push_str(kind.as_str());
120 }
121 report.push_str(":title=");
122 report.push_str(&title);
123 report.push_str(":msg=");
124 report.push_str(&message);
125 report.push_str("\x1b\\");
126 ensure!(report.len() <= 4096, "program status report exceeds byte limit");
127 Ok(report)
128}
129
130#[cfg(test)]
131mod tests;