#![forbid(unsafe_code)]
#![doc = include_str!("../Documentation.md")]
use kcode_k1_chat_boxes::{BoxId, ChatBox, ToolCallId};
use kcode_k1_chat_web_code_output::{WebCodeOutput, create, open};
use kcode_k1_web_code_document::{CodeDocument, Language};
use std::fmt::{Display, Formatter};
pub const NON_TEXT_SENTINEL: &str = "this file is a non-text object and cannot be modified";
#[derive(Clone, Debug, Eq, PartialEq)]
struct FileBinding {
originating_call: BoxId,
tool_call_id: ToolCallId,
message_index: u64,
path: String,
is_text: bool,
current: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceLayout {
bindings: Vec<FileBinding>,
}
impl SourceLayout {
pub fn originating_call(&self) -> Option<BoxId> {
self.bindings
.first()
.map(|binding| binding.originating_call)
}
pub fn tool_call_id(&self) -> Option<ToolCallId> {
self.bindings.first().map(|binding| binding.tool_call_id)
}
pub fn file_count(&self) -> usize {
self.bindings.len()
}
pub fn record_file(
&mut self,
originating_call: BoxId,
tool_call_id: ToolCallId,
path: String,
is_text: bool,
) -> Result<(), String> {
if self
.bindings
.iter()
.any(|binding| binding.current && binding.path == path)
{
return Err("source layout already contains the current path".to_owned());
}
self.bindings.push(FileBinding {
originating_call,
tool_call_id,
message_index: 1,
path,
is_text,
current: true,
});
Ok(())
}
pub fn retire_file(&mut self, path: &str) -> Result<(), String> {
let binding = self
.bindings
.iter_mut()
.rev()
.find(|binding| binding.current && binding.path == path)
.ok_or_else(|| "source layout does not contain the current path".to_owned())?;
binding.current = false;
Ok(())
}
}
pub type OpenLayout = SourceLayout;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourcePresentation {
pub output: WebCodeOutput,
pub layout: SourceLayout,
}
pub type OpenPresentation = SourcePresentation;
pub fn present_create(
originating_call: BoxId,
tool_call_id: ToolCallId,
document: &CodeDocument,
version: &str,
) -> SourcePresentation {
present(originating_call, tool_call_id, document, version, true)
}
pub fn present_open(
originating_call: BoxId,
tool_call_id: ToolCallId,
document: &CodeDocument,
version: &str,
) -> SourcePresentation {
present(originating_call, tool_call_id, document, version, false)
}
fn present(
originating_call: BoxId,
tool_call_id: ToolCallId,
document: &CodeDocument,
version: &str,
creating: bool,
) -> SourcePresentation {
let mut messages = Vec::with_capacity(document.files().len());
let mut paths = Vec::with_capacity(document.files().len());
let mut bindings = Vec::with_capacity(document.files().len());
for (offset, file) in document.files().iter().enumerate() {
let message_index = u64::try_from(offset + 1).expect("file message index fits u64");
let is_text = std::str::from_utf8(file.bytes()).is_ok();
messages.push(file_message(file.bytes()));
paths.push(file.path().to_owned());
bindings.push(FileBinding {
originating_call,
tool_call_id,
message_index,
path: file.path().to_owned(),
is_text,
current: true,
});
}
let name = document.family().logical_name();
let language = presentation_language(document);
let result = if creating {
Ok(format!("Created {version}."))
} else {
Ok(format!("Opened {version}."))
};
let output = if creating {
create(
tool_call_id,
messages,
result,
name,
version,
language,
&paths,
)
} else {
open(
tool_call_id,
messages,
result,
name,
version,
language,
&paths,
)
};
SourcePresentation {
output,
layout: SourceLayout { bindings },
}
}
fn presentation_language(document: &CodeDocument) -> Language {
let javascript = document.file("index.js").is_some();
let html = document.file("index.html").is_some();
let css = document.file("index.css").is_some();
match (javascript, html, css) {
(false, true, false) => Language::Html,
(false, false, true) => Language::Css,
_ => Language::JavaScript,
}
}
pub fn file_message(bytes: &[u8]) -> String {
std::str::from_utf8(bytes)
.map(str::to_owned)
.unwrap_or_else(|_| NON_TEXT_SENTINEL.to_owned())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileTarget {
path: String,
is_text: bool,
}
impl FileTarget {
pub fn path(&self) -> &str {
&self.path
}
pub const fn is_text(&self) -> bool {
self.is_text
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TargetRejection {
HistoryAbsent,
HistoryDuplicate,
MalformedPayload,
DifferentOpen,
MessageOutOfLayout,
NotSourceMessage,
RetiredFile,
}
impl TargetRejection {
pub const fn code(self) -> &'static str {
match self {
Self::HistoryAbsent => "history_absent",
Self::HistoryDuplicate => "history_duplicate",
Self::MalformedPayload => "malformed_payload",
Self::DifferentOpen => "different_open",
Self::MessageOutOfLayout => "message_out_of_layout",
Self::NotSourceMessage => "not_source_message",
Self::RetiredFile => "retired_file",
}
}
}
impl Display for TargetRejection {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::HistoryAbsent => "target box is absent from history",
Self::HistoryDuplicate => "target box ID occurs more than once in history",
Self::MalformedPayload => "target has malformed tool metadata",
Self::DifferentOpen => "target belongs to a different source generation",
Self::MessageOutOfLayout => "target message index is outside the source layout",
Self::NotSourceMessage => "target is not a source file message",
Self::RetiredFile => "target no longer represents a current file",
})
}
}
impl std::error::Error for TargetRejection {}
pub fn classify_anchor(
history: &[ChatBox],
target_id: BoxId,
layout: &SourceLayout,
) -> Result<FileTarget, TargetRejection> {
classify(history, target_id, layout, false)
}
pub fn classify_file_target(
history: &[ChatBox],
target_id: BoxId,
layout: &SourceLayout,
) -> Result<FileTarget, TargetRejection> {
classify(history, target_id, layout, true)
}
fn classify(
history: &[ChatBox],
target_id: BoxId,
layout: &SourceLayout,
require_current: bool,
) -> Result<FileTarget, TargetRejection> {
let mut matching = history.iter().filter(|value| value.id() == target_id);
let target = matching.next().ok_or(TargetRejection::HistoryAbsent)?;
if matching.next().is_some() {
return Err(TargetRejection::HistoryDuplicate);
}
let message = target
.tool_message_metadata()
.map_err(|_| TargetRejection::MalformedPayload)?
.ok_or(TargetRejection::NotSourceMessage)?;
let same_call = layout.bindings.iter().any(|binding| {
binding.tool_call_id == message.tool_call_id
&& binding.originating_call == message.originating_call
});
if !same_call {
return Err(TargetRejection::DifferentOpen);
}
let binding = layout
.bindings
.iter()
.find(|binding| {
binding.tool_call_id == message.tool_call_id
&& binding.originating_call == message.originating_call
&& binding.message_index == message.message_index
})
.ok_or(TargetRejection::MessageOutOfLayout)?;
if require_current && !binding.current {
return Err(TargetRejection::RetiredFile);
}
Ok(FileTarget {
path: binding.path.clone(),
is_text: binding.is_text,
})
}
#[cfg(test)]
mod tests {
use super::*;
use kcode_k1_chat_boxes::{ToolMessageMetadata, named_tool_message_box};
use kcode_k1_transaction_id::TxId;
use kcode_k1_web_package::{AuthorityId, SourceFile, WebFamily};
fn call(byte: u8) -> ToolCallId {
ToolCallId::new([byte; 12], 4)
}
fn document() -> CodeDocument {
CodeDocument::new(
WebFamily::new(AuthorityId::new(TxId::from_bytes([1; 12])), "alpha").unwrap(),
vec![
SourceFile::new("z/binary.dat", vec![0xff, 0x00]),
SourceFile::new("Documentation.md", b"Docs\r\n".to_vec()),
SourceFile::new("src/a.odd", b"export const value = 1;\n".to_vec()),
],
)
.unwrap()
}
fn assign(id: u64, value: ChatBox) -> ChatBox {
ChatBox::new(
BoxId::new(id),
value.box_type().to_owned(),
value.contents().to_owned(),
value.hidden_type().to_owned(),
value.hidden_contents().to_owned(),
)
}
fn message(
id: u64,
originating_call: BoxId,
tool_call_id: ToolCallId,
index: u64,
title: &str,
body: &str,
) -> ChatBox {
assign(
id,
named_tool_message_box(
&ToolMessageMetadata {
tool_call_id,
originating_call,
message_index: index,
message: body.to_owned(),
},
title,
)
.unwrap(),
)
}
#[test]
fn complete_files_are_sorted_named_and_binary_safe() {
let opened = present_open(BoxId::new(7), call(2), &document(), "1.2.3");
assert_eq!(
opened.output.message_titles,
[
Some("File: Documentation.md".to_owned()),
Some("File: src/a.odd".to_owned()),
Some("File: z/binary.dat".to_owned()),
]
);
assert_eq!(opened.output.messages[0], "Docs\r\n");
assert_eq!(opened.output.messages[1], "export const value = 1;\n");
assert_eq!(opened.output.messages[2], NON_TEXT_SENTINEL);
assert_eq!(opened.layout.file_count(), 3);
}
#[test]
fn retired_and_incremental_messages_keep_anchor_semantics() {
let mut layout = present_create(BoxId::new(7), call(2), &document(), "draft").layout;
let docs = message(
10,
BoxId::new(7),
call(2),
1,
"File: Documentation.md",
"Docs\r\n",
);
let binary = message(
11,
BoxId::new(7),
call(2),
3,
"File: z/binary.dat",
NON_TEXT_SENTINEL,
);
layout.retire_file("z/binary.dat").unwrap();
assert_eq!(
classify_anchor(std::slice::from_ref(&binary), BoxId::new(11), &layout,)
.unwrap()
.path(),
"z/binary.dat"
);
assert_eq!(
classify_file_target(std::slice::from_ref(&binary), BoxId::new(11), &layout,),
Err(TargetRejection::RetiredFile)
);
assert!(
classify_file_target(std::slice::from_ref(&docs), BoxId::new(10), &layout,)
.unwrap()
.is_text()
);
layout
.record_file(BoxId::new(20), call(3), "new/file.txt".to_owned(), true)
.unwrap();
let added = message(21, BoxId::new(20), call(3), 1, "File: new/file.txt", "new");
assert_eq!(
classify_file_target(std::slice::from_ref(&added), BoxId::new(21), &layout,)
.unwrap()
.path(),
"new/file.txt"
);
}
}