mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: wire spreadsheet artifact (#13362)
This commit is contained in:
committed by
GitHub
Unverified
parent
24ba01b9da
commit
8159f05dfd
Generated
+1
@@ -1802,6 +1802,7 @@ dependencies = [
|
|||||||
"codex-apply-patch",
|
"codex-apply-patch",
|
||||||
"codex-arg0",
|
"codex-arg0",
|
||||||
"codex-artifact-presentation",
|
"codex-artifact-presentation",
|
||||||
|
"codex-artifact-spreadsheet",
|
||||||
"codex-async-utils",
|
"codex-async-utils",
|
||||||
"codex-client",
|
"codex-client",
|
||||||
"codex-config",
|
"codex-config",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub struct SpreadsheetChart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct SpreadsheetChartLookup<'a> {
|
pub struct SpreadsheetChartLookup {
|
||||||
pub id: Option<u32>,
|
pub id: Option<u32>,
|
||||||
pub index: Option<usize>,
|
pub index: Option<usize>,
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,6 @@ impl SpreadsheetSheet {
|
|||||||
.transpose()
|
.transpose()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.flatten()
|
|
||||||
.is_some_and(|chart_range| chart_range.intersects(target))
|
.is_some_and(|chart_range| chart_range.intersects(target))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -112,7 +111,7 @@ impl SpreadsheetSheet {
|
|||||||
pub fn get_chart(
|
pub fn get_chart(
|
||||||
&self,
|
&self,
|
||||||
action: &str,
|
action: &str,
|
||||||
lookup: SpreadsheetChartLookup<'_>,
|
lookup: SpreadsheetChartLookup,
|
||||||
) -> Result<&SpreadsheetChart, SpreadsheetArtifactError> {
|
) -> Result<&SpreadsheetChart, SpreadsheetArtifactError> {
|
||||||
if let Some(id) = lookup.id {
|
if let Some(id) = lookup.id {
|
||||||
return self
|
return self
|
||||||
@@ -221,7 +220,7 @@ impl SpreadsheetSheet {
|
|||||||
pub fn add_chart_series(
|
pub fn add_chart_series(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &str,
|
action: &str,
|
||||||
lookup: SpreadsheetChartLookup<'_>,
|
lookup: SpreadsheetChartLookup,
|
||||||
mut series: SpreadsheetChartSeries,
|
mut series: SpreadsheetChartSeries,
|
||||||
) -> Result<u32, SpreadsheetArtifactError> {
|
) -> Result<u32, SpreadsheetArtifactError> {
|
||||||
validate_chart_series(action, &series)?;
|
validate_chart_series(action, &series)?;
|
||||||
@@ -235,7 +234,7 @@ impl SpreadsheetSheet {
|
|||||||
pub fn delete_chart(
|
pub fn delete_chart(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &str,
|
action: &str,
|
||||||
lookup: SpreadsheetChartLookup<'_>,
|
lookup: SpreadsheetChartLookup,
|
||||||
) -> Result<(), SpreadsheetArtifactError> {
|
) -> Result<(), SpreadsheetArtifactError> {
|
||||||
let index = if let Some(id) = lookup.id {
|
let index = if let Some(id) = lookup.id {
|
||||||
self.charts
|
self.charts
|
||||||
@@ -267,7 +266,7 @@ impl SpreadsheetSheet {
|
|||||||
pub fn set_chart_properties(
|
pub fn set_chart_properties(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &str,
|
action: &str,
|
||||||
lookup: SpreadsheetChartLookup<'_>,
|
lookup: SpreadsheetChartLookup,
|
||||||
properties: SpreadsheetChartProperties,
|
properties: SpreadsheetChartProperties,
|
||||||
) -> Result<(), SpreadsheetArtifactError> {
|
) -> Result<(), SpreadsheetArtifactError> {
|
||||||
let chart = self.get_chart_mut(action, lookup)?;
|
let chart = self.get_chart_mut(action, lookup)?;
|
||||||
@@ -307,7 +306,7 @@ impl SpreadsheetSheet {
|
|||||||
fn get_chart_mut(
|
fn get_chart_mut(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &str,
|
action: &str,
|
||||||
lookup: SpreadsheetChartLookup<'_>,
|
lookup: SpreadsheetChartLookup,
|
||||||
) -> Result<&mut SpreadsheetChart, SpreadsheetArtifactError> {
|
) -> Result<&mut SpreadsheetChart, SpreadsheetArtifactError> {
|
||||||
if let Some(id) = lookup.id {
|
if let Some(id) = lookup.id {
|
||||||
return self
|
return self
|
||||||
@@ -320,11 +319,12 @@ impl SpreadsheetSheet {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(index) = lookup.index {
|
if let Some(index) = lookup.index {
|
||||||
|
let len = self.charts.len();
|
||||||
return self.charts.get_mut(index).ok_or_else(|| {
|
return self.charts.get_mut(index).ok_or_else(|| {
|
||||||
SpreadsheetArtifactError::IndexOutOfRange {
|
SpreadsheetArtifactError::IndexOutOfRange {
|
||||||
action: action.to_string(),
|
action: action.to_string(),
|
||||||
index,
|
index,
|
||||||
len: self.charts.len(),
|
len,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,18 @@ impl SpreadsheetConditionalFormatCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SpreadsheetArtifact {
|
impl SpreadsheetArtifact {
|
||||||
|
pub fn validate_conditional_formats(
|
||||||
|
&self,
|
||||||
|
action: &str,
|
||||||
|
sheet_name: &str,
|
||||||
|
) -> Result<(), SpreadsheetArtifactError> {
|
||||||
|
let sheet = self.sheet_lookup(action, Some(sheet_name), None)?;
|
||||||
|
for format in &sheet.conditional_formats {
|
||||||
|
validate_conditional_format(self, format, action)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_conditional_format(
|
pub fn add_conditional_format(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &str,
|
action: &str,
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
mod address;
|
mod address;
|
||||||
|
mod chart;
|
||||||
|
mod conditional;
|
||||||
mod error;
|
mod error;
|
||||||
mod formula;
|
mod formula;
|
||||||
mod manager;
|
mod manager;
|
||||||
mod model;
|
mod model;
|
||||||
|
mod pivot;
|
||||||
mod render;
|
mod render;
|
||||||
mod style;
|
mod style;
|
||||||
|
mod table;
|
||||||
mod xlsx;
|
mod xlsx;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use address::*;
|
pub use address::*;
|
||||||
|
pub use chart::*;
|
||||||
|
pub use conditional::*;
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
pub use model::*;
|
pub use model::*;
|
||||||
|
pub use pivot::*;
|
||||||
pub use render::*;
|
pub use render::*;
|
||||||
pub use style::*;
|
pub use style::*;
|
||||||
|
pub use table::*;
|
||||||
|
|||||||
@@ -113,6 +113,26 @@ impl SpreadsheetArtifactManager {
|
|||||||
"list_sheets" => self.list_sheets(request),
|
"list_sheets" => self.list_sheets(request),
|
||||||
"get_sheet" => self.get_sheet(request),
|
"get_sheet" => self.get_sheet(request),
|
||||||
"inspect" => self.inspect(request),
|
"inspect" => self.inspect(request),
|
||||||
|
"list_charts" => self.list_charts(request),
|
||||||
|
"get_chart" => self.get_chart(request),
|
||||||
|
"create_chart" => self.create_chart(request),
|
||||||
|
"add_chart_series" => self.add_chart_series(request),
|
||||||
|
"set_chart_properties" => self.set_chart_properties(request),
|
||||||
|
"delete_chart" => self.delete_chart(request),
|
||||||
|
"list_tables" => self.list_tables(request),
|
||||||
|
"get_table" => self.get_table(request),
|
||||||
|
"create_table" => self.create_table(request),
|
||||||
|
"set_table_style" => self.set_table_style(request),
|
||||||
|
"clear_table_filters" => self.clear_table_filters(request),
|
||||||
|
"reapply_table_filters" => self.reapply_table_filters(request),
|
||||||
|
"rename_table_column" => self.rename_table_column(request),
|
||||||
|
"set_table_column_totals" => self.set_table_column_totals(request),
|
||||||
|
"delete_table" => self.delete_table(request),
|
||||||
|
"list_conditional_formats" => self.list_conditional_formats(request),
|
||||||
|
"add_conditional_format" => self.add_conditional_format(request),
|
||||||
|
"delete_conditional_format" => self.delete_conditional_format(request),
|
||||||
|
"list_pivot_tables" => self.list_pivot_tables(request),
|
||||||
|
"get_pivot_table" => self.get_pivot_table(request),
|
||||||
"create_sheet" => self.create_sheet(request),
|
"create_sheet" => self.create_sheet(request),
|
||||||
"rename_sheet" => self.rename_sheet(request),
|
"rename_sheet" => self.rename_sheet(request),
|
||||||
"delete_sheet" => self.delete_sheet(request),
|
"delete_sheet" => self.delete_sheet(request),
|
||||||
@@ -493,6 +513,632 @@ impl SpreadsheetArtifactManager {
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_charts(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: SheetLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact(&artifact_id, &request.action)?;
|
||||||
|
let sheet = artifact.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let range = args.range.as_deref().map(CellRange::parse).transpose()?;
|
||||||
|
let charts = sheet.list_charts(range.as_ref())?;
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Listed {} charts on `{}`", charts.len(), sheet.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_ref = Some(sheet_reference(sheet));
|
||||||
|
response.range_ref = range
|
||||||
|
.as_ref()
|
||||||
|
.map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry));
|
||||||
|
response.chart_list = Some(charts);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_chart(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: ChartLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact(&artifact_id, &request.action)?;
|
||||||
|
let sheet = artifact.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let chart = sheet
|
||||||
|
.get_chart(&request.action, chart_lookup_from_args(&args))?
|
||||||
|
.clone();
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Retrieved chart `{}` from `{}`", chart.id, sheet.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_ref = Some(sheet_reference(sheet));
|
||||||
|
response.chart_list = Some(vec![chart]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_chart(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: CreateChartArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
if let Some(source_sheet_name) = args.source_sheet_name.as_deref() {
|
||||||
|
artifact.sheet_lookup(&request.action, Some(source_sheet_name), None)?;
|
||||||
|
}
|
||||||
|
let source_range = CellRange::parse(&args.source_range)?;
|
||||||
|
let chart = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let chart_id = sheet.create_chart(
|
||||||
|
&request.action,
|
||||||
|
args.chart_type,
|
||||||
|
args.source_sheet_name.or_else(|| Some(sheet.name.clone())),
|
||||||
|
&source_range,
|
||||||
|
args.options.unwrap_or_default(),
|
||||||
|
)?;
|
||||||
|
sheet
|
||||||
|
.get_chart(
|
||||||
|
&request.action,
|
||||||
|
crate::SpreadsheetChartLookup {
|
||||||
|
id: Some(chart_id),
|
||||||
|
index: None,
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Created chart `{}`", chart.id),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.chart_list = Some(vec![chart]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_chart_series(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: AddChartSeriesArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let chart = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let series_id = sheet.add_chart_series(
|
||||||
|
&request.action,
|
||||||
|
crate::SpreadsheetChartLookup {
|
||||||
|
id: args.chart_id,
|
||||||
|
index: args.chart_index.map(|value| value as usize),
|
||||||
|
},
|
||||||
|
args.series,
|
||||||
|
)?;
|
||||||
|
let chart = sheet.get_chart(
|
||||||
|
&request.action,
|
||||||
|
crate::SpreadsheetChartLookup {
|
||||||
|
id: args.chart_id.or(Some(series_id).and(None)),
|
||||||
|
index: args.chart_index.map(|value| value as usize),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
match chart {
|
||||||
|
Ok(chart) => chart.clone(),
|
||||||
|
Err(_) => sheet
|
||||||
|
.get_chart(
|
||||||
|
&request.action,
|
||||||
|
crate::SpreadsheetChartLookup {
|
||||||
|
id: None,
|
||||||
|
index: args.chart_index.map(|value| value as usize),
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
.clone(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Added chart series to chart `{}`", chart.id),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.chart_list = Some(vec![chart]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_chart_properties(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: SetChartPropertiesArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let chart = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.lookup.sheet_name.as_deref(),
|
||||||
|
args.lookup.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let lookup = chart_lookup_from_args(&args.lookup);
|
||||||
|
sheet.set_chart_properties(&request.action, lookup.clone(), args.properties)?;
|
||||||
|
sheet.get_chart(&request.action, lookup)?.clone()
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Updated chart `{}`", chart.id),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.chart_list = Some(vec![chart]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_chart(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: ChartLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let sheet_name = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let name = sheet.name.clone();
|
||||||
|
sheet.delete_chart(&request.action, chart_lookup_from_args(&args))?;
|
||||||
|
name
|
||||||
|
};
|
||||||
|
let action = request.action.clone();
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
action.clone(),
|
||||||
|
format!("Deleted chart from `{sheet_name}`"),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_list = Some(vec![
|
||||||
|
artifact
|
||||||
|
.sheet_lookup(&action, Some(&sheet_name), None)?
|
||||||
|
.summary(),
|
||||||
|
]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_tables(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: SheetLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact(&artifact_id, &request.action)?;
|
||||||
|
let sheet = artifact.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let range = args.range.as_deref().map(CellRange::parse).transpose()?;
|
||||||
|
let tables = sheet.list_tables(range.as_ref())?;
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Listed {} tables on `{}`", tables.len(), sheet.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_ref = Some(sheet_reference(sheet));
|
||||||
|
response.range_ref = range
|
||||||
|
.as_ref()
|
||||||
|
.map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry));
|
||||||
|
response.table_list = Some(tables);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_table(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: TableLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact(&artifact_id, &request.action)?;
|
||||||
|
let sheet = artifact.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let table = sheet.get_table_view(&request.action, table_lookup_from_args(&args))?;
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Retrieved table `{}` from `{}`", table.name, sheet.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_ref = Some(sheet_reference(sheet));
|
||||||
|
response.table_list = Some(vec![table]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_table(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: CreateTableArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let range = CellRange::parse(&args.range)?;
|
||||||
|
let table = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let table_id = sheet.create_table(&request.action, &range, args.options)?;
|
||||||
|
sheet.get_table_view(
|
||||||
|
&request.action,
|
||||||
|
crate::SpreadsheetTableLookup {
|
||||||
|
name: None,
|
||||||
|
display_name: None,
|
||||||
|
id: Some(table_id),
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Created table `{}`", table.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.table_list = Some(vec![table]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_table_style(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: SetTableStyleArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let table = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.lookup.sheet_name.as_deref(),
|
||||||
|
args.lookup.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let lookup = table_lookup_from_args(&args.lookup);
|
||||||
|
sheet.set_table_style(&request.action, lookup.clone(), args.options)?;
|
||||||
|
sheet.get_table_view(&request.action, lookup)?
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Updated table style for `{}`", table.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.table_list = Some(vec![table]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_table_filters(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: TableLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let table = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let lookup = table_lookup_from_args(&args);
|
||||||
|
sheet.clear_table_filters(&request.action, lookup.clone())?;
|
||||||
|
sheet.get_table_view(&request.action, lookup)?
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Cleared table filters for `{}`", table.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.table_list = Some(vec![table]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reapply_table_filters(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: TableLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let table = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let lookup = table_lookup_from_args(&args);
|
||||||
|
sheet.reapply_table_filters(&request.action, lookup.clone())?;
|
||||||
|
sheet.get_table_view(&request.action, lookup)?
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Reapplied table filters for `{}`", table.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.table_list = Some(vec![table]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_table_column(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: RenameTableColumnArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let column = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.lookup.sheet_name.as_deref(),
|
||||||
|
args.lookup.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
sheet.rename_table_column(
|
||||||
|
&request.action,
|
||||||
|
table_lookup_from_args(&args.lookup),
|
||||||
|
args.column_id,
|
||||||
|
args.column_name.as_deref(),
|
||||||
|
args.new_name,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Renamed table column to `{}`", column.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.serialized_dict = Some(to_serialized_value(column)?);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_table_column_totals(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: SetTableColumnTotalsArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let column = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.lookup.sheet_name.as_deref(),
|
||||||
|
args.lookup.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
sheet.set_table_column_totals(
|
||||||
|
&request.action,
|
||||||
|
table_lookup_from_args(&args.lookup),
|
||||||
|
args.column_id,
|
||||||
|
args.column_name.as_deref(),
|
||||||
|
args.totals_row_label,
|
||||||
|
args.totals_row_function,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Updated totals metadata for column `{}`", column.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.serialized_dict = Some(to_serialized_value(column)?);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_table(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: TableLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let sheet_name = {
|
||||||
|
let sheet = artifact.sheet_lookup_mut(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let name = sheet.name.clone();
|
||||||
|
sheet.delete_table(&request.action, table_lookup_from_args(&args))?;
|
||||||
|
name
|
||||||
|
};
|
||||||
|
let action = request.action.clone();
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
action.clone(),
|
||||||
|
format!("Deleted table from `{sheet_name}`"),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_list = Some(vec![
|
||||||
|
artifact
|
||||||
|
.sheet_lookup(&action, Some(&sheet_name), None)?
|
||||||
|
.summary(),
|
||||||
|
]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_conditional_formats(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: SheetLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact(&artifact_id, &request.action)?;
|
||||||
|
let sheet = artifact.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let range = args.range.as_deref().map(CellRange::parse).transpose()?;
|
||||||
|
let formats = sheet.list_conditional_formats(range.as_ref());
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!(
|
||||||
|
"Listed {} conditional formats on `{}`",
|
||||||
|
formats.len(),
|
||||||
|
sheet.name
|
||||||
|
),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_ref = Some(sheet_reference(sheet));
|
||||||
|
response.range_ref = range
|
||||||
|
.as_ref()
|
||||||
|
.map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry));
|
||||||
|
response.conditional_format_list = Some(formats);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_conditional_format(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: AddConditionalFormatArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let sheet_name = artifact
|
||||||
|
.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?
|
||||||
|
.name
|
||||||
|
.clone();
|
||||||
|
let format_id =
|
||||||
|
artifact.add_conditional_format(&request.action, &sheet_name, args.format)?;
|
||||||
|
let format = artifact
|
||||||
|
.sheet_lookup(&request.action, Some(&sheet_name), None)?
|
||||||
|
.conditional_formats
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.id == format_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| SpreadsheetArtifactError::Serialization {
|
||||||
|
message: format!("created conditional format `{format_id}` was not available"),
|
||||||
|
})?;
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Created conditional format `{format_id}`"),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.conditional_format_list = Some(vec![format]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_conditional_format(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: DeleteConditionalFormatArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
|
||||||
|
let sheet_name = artifact
|
||||||
|
.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?
|
||||||
|
.name
|
||||||
|
.clone();
|
||||||
|
artifact.delete_conditional_format(&request.action, &sheet_name, args.id)?;
|
||||||
|
let action = request.action.clone();
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
action.clone(),
|
||||||
|
format!("Deleted conditional format `{}`", args.id),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_list = Some(vec![
|
||||||
|
artifact
|
||||||
|
.sheet_lookup(&action, Some(&sheet_name), None)?
|
||||||
|
.summary(),
|
||||||
|
]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_pivot_tables(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: SheetLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact(&artifact_id, &request.action)?;
|
||||||
|
let sheet = artifact.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let range = args.range.as_deref().map(CellRange::parse).transpose()?;
|
||||||
|
let pivots = sheet.list_pivot_tables(range.as_ref())?;
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!("Listed {} pivot tables on `{}`", pivots.len(), sheet.name),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_ref = Some(sheet_reference(sheet));
|
||||||
|
response.range_ref = range
|
||||||
|
.as_ref()
|
||||||
|
.map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry));
|
||||||
|
response.pivot_table_list = Some(pivots);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pivot_table(
|
||||||
|
&mut self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let args: PivotTableLookupArgs = parse_args(&request.action, &request.args)?;
|
||||||
|
let artifact_id = required_artifact_id(&request)?;
|
||||||
|
let artifact = self.get_artifact(&artifact_id, &request.action)?;
|
||||||
|
let sheet = artifact.sheet_lookup(
|
||||||
|
&request.action,
|
||||||
|
args.sheet_name.as_deref(),
|
||||||
|
args.sheet_index.map(|value| value as usize),
|
||||||
|
)?;
|
||||||
|
let pivot = sheet
|
||||||
|
.get_pivot_table(&request.action, pivot_lookup_from_args(&args))?
|
||||||
|
.clone();
|
||||||
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
|
artifact_id,
|
||||||
|
request.action,
|
||||||
|
format!(
|
||||||
|
"Retrieved pivot table `{}` from `{}`",
|
||||||
|
pivot.name, sheet.name
|
||||||
|
),
|
||||||
|
snapshot_for_artifact(artifact),
|
||||||
|
);
|
||||||
|
response.sheet_ref = Some(sheet_reference(sheet));
|
||||||
|
response.pivot_table_list = Some(vec![pivot]);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
fn rename_sheet(
|
fn rename_sheet(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: SpreadsheetArtifactRequest,
|
request: SpreadsheetArtifactRequest,
|
||||||
@@ -776,6 +1422,7 @@ impl SpreadsheetArtifactManager {
|
|||||||
sheet.cleanup_and_validate_sheet()?;
|
sheet.cleanup_and_validate_sheet()?;
|
||||||
sheet.summary()
|
sheet.summary()
|
||||||
};
|
};
|
||||||
|
artifact.validate_conditional_formats(&request.action, &sheet_summary.name)?;
|
||||||
let mut response = SpreadsheetArtifactResponse::new(
|
let mut response = SpreadsheetArtifactResponse::new(
|
||||||
artifact_id,
|
artifact_id,
|
||||||
request.action,
|
request.action,
|
||||||
@@ -2006,6 +2653,14 @@ pub struct SpreadsheetArtifactResponse {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub sheet_list: Option<Vec<SpreadsheetSheetSummary>>,
|
pub sheet_list: Option<Vec<SpreadsheetSheetSummary>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub chart_list: Option<Vec<crate::SpreadsheetChart>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub table_list: Option<Vec<crate::SpreadsheetTableView>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub conditional_format_list: Option<Vec<crate::SpreadsheetConditionalFormat>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pivot_table_list: Option<Vec<crate::SpreadsheetPivotTable>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub sheet_ref: Option<SpreadsheetSheetRef>,
|
pub sheet_ref: Option<SpreadsheetSheetRef>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub cell_ref: Option<SpreadsheetCellRef>,
|
pub cell_ref: Option<SpreadsheetCellRef>,
|
||||||
@@ -2056,6 +2711,10 @@ impl SpreadsheetArtifactResponse {
|
|||||||
artifact_snapshot: Some(artifact_snapshot),
|
artifact_snapshot: Some(artifact_snapshot),
|
||||||
workbook_summary: None,
|
workbook_summary: None,
|
||||||
sheet_list: None,
|
sheet_list: None,
|
||||||
|
chart_list: None,
|
||||||
|
table_list: None,
|
||||||
|
conditional_format_list: None,
|
||||||
|
pivot_table_list: None,
|
||||||
sheet_ref: None,
|
sheet_ref: None,
|
||||||
cell_ref: None,
|
cell_ref: None,
|
||||||
range_ref: None,
|
range_ref: None,
|
||||||
@@ -2097,6 +2756,10 @@ pub struct SpreadsheetSheetSnapshot {
|
|||||||
pub filled_columns: usize,
|
pub filled_columns: usize,
|
||||||
pub minimum_range_filled: String,
|
pub minimum_range_filled: String,
|
||||||
pub merged_range_count: usize,
|
pub merged_range_count: usize,
|
||||||
|
pub chart_count: usize,
|
||||||
|
pub table_count: usize,
|
||||||
|
pub conditional_format_count: usize,
|
||||||
|
pub pivot_table_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -2137,6 +2800,105 @@ struct SheetLookupArgs {
|
|||||||
range: Option<String>,
|
range: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ChartLookupArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
chart_id: Option<u32>,
|
||||||
|
chart_index: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct CreateChartArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
chart_type: crate::SpreadsheetChartType,
|
||||||
|
source_sheet_name: Option<String>,
|
||||||
|
source_range: String,
|
||||||
|
options: Option<crate::SpreadsheetChartCreateOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AddChartSeriesArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
chart_id: Option<u32>,
|
||||||
|
chart_index: Option<u32>,
|
||||||
|
series: crate::SpreadsheetChartSeries,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SetChartPropertiesArgs {
|
||||||
|
#[serde(flatten)]
|
||||||
|
lookup: ChartLookupArgs,
|
||||||
|
properties: crate::SpreadsheetChartProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TableLookupArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
name: Option<String>,
|
||||||
|
display_name: Option<String>,
|
||||||
|
id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct CreateTableArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
range: String,
|
||||||
|
options: crate::SpreadsheetCreateTableOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SetTableStyleArgs {
|
||||||
|
#[serde(flatten)]
|
||||||
|
lookup: TableLookupArgs,
|
||||||
|
options: crate::SpreadsheetTableStyleOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct RenameTableColumnArgs {
|
||||||
|
#[serde(flatten)]
|
||||||
|
lookup: TableLookupArgs,
|
||||||
|
column_id: Option<u32>,
|
||||||
|
column_name: Option<String>,
|
||||||
|
new_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SetTableColumnTotalsArgs {
|
||||||
|
#[serde(flatten)]
|
||||||
|
lookup: TableLookupArgs,
|
||||||
|
column_id: Option<u32>,
|
||||||
|
column_name: Option<String>,
|
||||||
|
totals_row_label: Option<String>,
|
||||||
|
totals_row_function: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AddConditionalFormatArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
format: crate::SpreadsheetConditionalFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DeleteConditionalFormatArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct PivotTableLookupArgs {
|
||||||
|
sheet_name: Option<String>,
|
||||||
|
sheet_index: Option<u32>,
|
||||||
|
name: Option<String>,
|
||||||
|
index: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct CreateSheetArgs {
|
struct CreateSheetArgs {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -2444,11 +3206,37 @@ fn snapshot_for_artifact(artifact: &SpreadsheetArtifact) -> SpreadsheetArtifactS
|
|||||||
filled_columns: sheet.filled_columns(),
|
filled_columns: sheet.filled_columns(),
|
||||||
minimum_range_filled: sheet.minimum_range_filled(),
|
minimum_range_filled: sheet.minimum_range_filled(),
|
||||||
merged_range_count: sheet.merged_ranges.len(),
|
merged_range_count: sheet.merged_ranges.len(),
|
||||||
|
chart_count: sheet.charts.len(),
|
||||||
|
table_count: sheet.tables.len(),
|
||||||
|
conditional_format_count: sheet.conditional_formats.len(),
|
||||||
|
pivot_table_count: sheet.pivot_tables.len(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn chart_lookup_from_args(args: &ChartLookupArgs) -> crate::SpreadsheetChartLookup {
|
||||||
|
crate::SpreadsheetChartLookup {
|
||||||
|
id: args.chart_id,
|
||||||
|
index: args.chart_index.map(|value| value as usize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_lookup_from_args(args: &TableLookupArgs) -> crate::SpreadsheetTableLookup<'_> {
|
||||||
|
crate::SpreadsheetTableLookup {
|
||||||
|
name: args.name.as_deref(),
|
||||||
|
display_name: args.display_name.as_deref(),
|
||||||
|
id: args.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pivot_lookup_from_args(args: &PivotTableLookupArgs) -> crate::SpreadsheetPivotTableLookup<'_> {
|
||||||
|
crate::SpreadsheetPivotTableLookup {
|
||||||
|
name: args.name.as_deref(),
|
||||||
|
index: args.index.map(|value| value as usize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sheet_reference(sheet: &crate::SpreadsheetSheet) -> SpreadsheetSheetRef {
|
fn sheet_reference(sheet: &crate::SpreadsheetSheet) -> SpreadsheetSheetRef {
|
||||||
SpreadsheetSheetRef {
|
SpreadsheetSheetRef {
|
||||||
sheet_name: sheet.name.clone(),
|
sheet_name: sheet.name.clone(),
|
||||||
|
|||||||
@@ -457,6 +457,10 @@ pub struct SpreadsheetSheetSummary {
|
|||||||
pub default_column_width: Option<f64>,
|
pub default_column_width: Option<f64>,
|
||||||
pub show_grid_lines: bool,
|
pub show_grid_lines: bool,
|
||||||
pub merged_range_count: usize,
|
pub merged_range_count: usize,
|
||||||
|
pub chart_count: usize,
|
||||||
|
pub table_count: usize,
|
||||||
|
pub conditional_format_count: usize,
|
||||||
|
pub pivot_table_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -504,6 +508,14 @@ pub struct SpreadsheetSheet {
|
|||||||
pub cells: BTreeMap<CellAddress, SpreadsheetCell>,
|
pub cells: BTreeMap<CellAddress, SpreadsheetCell>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub merged_ranges: Vec<CellRange>,
|
pub merged_ranges: Vec<CellRange>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub charts: Vec<crate::SpreadsheetChart>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub tables: Vec<crate::SpreadsheetTable>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub conditional_formats: Vec<crate::SpreadsheetConditionalFormat>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pivot_tables: Vec<crate::SpreadsheetPivotTable>,
|
||||||
pub default_row_height: Option<f64>,
|
pub default_row_height: Option<f64>,
|
||||||
pub default_column_width: Option<f64>,
|
pub default_column_width: Option<f64>,
|
||||||
pub show_grid_lines: bool,
|
pub show_grid_lines: bool,
|
||||||
@@ -569,6 +581,10 @@ impl SpreadsheetSheet {
|
|||||||
name,
|
name,
|
||||||
cells: BTreeMap::new(),
|
cells: BTreeMap::new(),
|
||||||
merged_ranges: Vec::new(),
|
merged_ranges: Vec::new(),
|
||||||
|
charts: Vec::new(),
|
||||||
|
tables: Vec::new(),
|
||||||
|
conditional_formats: Vec::new(),
|
||||||
|
pivot_tables: Vec::new(),
|
||||||
default_row_height: None,
|
default_row_height: None,
|
||||||
default_column_width: None,
|
default_column_width: None,
|
||||||
show_grid_lines: true,
|
show_grid_lines: true,
|
||||||
@@ -679,6 +695,10 @@ impl SpreadsheetSheet {
|
|||||||
default_column_width: self.default_column_width,
|
default_column_width: self.default_column_width,
|
||||||
show_grid_lines: self.show_grid_lines,
|
show_grid_lines: self.show_grid_lines,
|
||||||
merged_range_count: self.merged_ranges.len(),
|
merged_range_count: self.merged_ranges.len(),
|
||||||
|
chart_count: self.charts.len(),
|
||||||
|
table_count: self.tables.len(),
|
||||||
|
conditional_format_count: self.conditional_formats.len(),
|
||||||
|
pivot_table_count: self.pivot_tables.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,6 +1323,9 @@ impl SpreadsheetSheet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.validate_tables("cleanup_and_validate_sheet")?;
|
||||||
|
self.validate_charts("cleanup_and_validate_sheet")?;
|
||||||
|
self.validate_pivot_tables("cleanup_and_validate_sheet")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1718,7 +1741,24 @@ impl SpreadsheetArtifact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match selected.as_str() {
|
match selected.as_str() {
|
||||||
"xlsx" => write_xlsx(self, path),
|
"xlsx" => {
|
||||||
|
for sheet in &self.sheets {
|
||||||
|
if !sheet.charts.is_empty()
|
||||||
|
|| !sheet.tables.is_empty()
|
||||||
|
|| !sheet.conditional_formats.is_empty()
|
||||||
|
|| !sheet.pivot_tables.is_empty()
|
||||||
|
{
|
||||||
|
return Err(SpreadsheetArtifactError::ExportFailed {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
message: format!(
|
||||||
|
"xlsx export does not yet support charts, tables, conditional formats, or pivot tables on sheet `{}`; use json or bin export instead",
|
||||||
|
sheet.name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_xlsx(self, path)
|
||||||
|
}
|
||||||
"json" => {
|
"json" => {
|
||||||
let json = self.to_json()?;
|
let json = self.to_json()?;
|
||||||
std::fs::write(path, json).map_err(|error| {
|
std::fs::write(path, json).map_err(|error| {
|
||||||
|
|||||||
@@ -181,7 +181,12 @@ impl SpreadsheetSheet {
|
|||||||
range: &CellRange,
|
range: &CellRange,
|
||||||
options: SpreadsheetCreateTableOptions,
|
options: SpreadsheetCreateTableOptions,
|
||||||
) -> Result<u32, SpreadsheetArtifactError> {
|
) -> Result<u32, SpreadsheetArtifactError> {
|
||||||
validate_table_geometry(action, range, options.header_row_count, options.totals_row_count)?;
|
validate_table_geometry(
|
||||||
|
action,
|
||||||
|
range,
|
||||||
|
options.header_row_count,
|
||||||
|
options.totals_row_count,
|
||||||
|
)?;
|
||||||
for table in &self.tables {
|
for table in &self.tables {
|
||||||
let table_range = table.range()?;
|
let table_range = table.range()?;
|
||||||
if table_range.intersects(range) {
|
if table_range.intersects(range) {
|
||||||
@@ -368,7 +373,12 @@ impl SpreadsheetSheet {
|
|||||||
let mut seen_display_names = BTreeSet::new();
|
let mut seen_display_names = BTreeSet::new();
|
||||||
for table in &self.tables {
|
for table in &self.tables {
|
||||||
let range = table.range()?;
|
let range = table.range()?;
|
||||||
validate_table_geometry(action, &range, table.header_row_count, table.totals_row_count)?;
|
validate_table_geometry(
|
||||||
|
action,
|
||||||
|
&range,
|
||||||
|
table.header_row_count,
|
||||||
|
table.totals_row_count,
|
||||||
|
)?;
|
||||||
if !seen_names.insert(table.name.clone()) {
|
if !seen_names.insert(table.name.clone()) {
|
||||||
return Err(SpreadsheetArtifactError::InvalidArgs {
|
return Err(SpreadsheetArtifactError::InvalidArgs {
|
||||||
action: action.to_string(),
|
action: action.to_string(),
|
||||||
@@ -565,7 +575,8 @@ fn build_table_columns(
|
|||||||
|
|
||||||
fn unique_table_column_names(names: Vec<String>) -> Vec<String> {
|
fn unique_table_column_names(names: Vec<String>) -> Vec<String> {
|
||||||
let mut seen = BTreeMap::<String, u32>::new();
|
let mut seen = BTreeMap::<String, u32>::new();
|
||||||
names.into_iter()
|
names
|
||||||
|
.into_iter()
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
let entry = seen.entry(name.clone()).or_insert(0);
|
let entry = seen.entry(name.clone()).or_insert(0);
|
||||||
*entry += 1;
|
*entry += 1;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ codex-keyring-store = { workspace = true }
|
|||||||
codex-network-proxy = { workspace = true }
|
codex-network-proxy = { workspace = true }
|
||||||
codex-otel = { workspace = true }
|
codex-otel = { workspace = true }
|
||||||
codex-artifact-presentation = { workspace = true }
|
codex-artifact-presentation = { workspace = true }
|
||||||
|
codex-artifact-spreadsheet = { workspace = true }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
codex-rmcp-client = { workspace = true }
|
codex-rmcp-client = { workspace = true }
|
||||||
codex-state = { workspace = true }
|
codex-state = { workspace = true }
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ use chrono::Utc;
|
|||||||
use codex_artifact_presentation::PresentationArtifactError;
|
use codex_artifact_presentation::PresentationArtifactError;
|
||||||
use codex_artifact_presentation::PresentationArtifactExecutionRequest;
|
use codex_artifact_presentation::PresentationArtifactExecutionRequest;
|
||||||
use codex_artifact_presentation::PresentationArtifactResponse;
|
use codex_artifact_presentation::PresentationArtifactResponse;
|
||||||
|
use codex_artifact_spreadsheet::SpreadsheetArtifactError;
|
||||||
|
use codex_artifact_spreadsheet::SpreadsheetArtifactRequest;
|
||||||
|
use codex_artifact_spreadsheet::SpreadsheetArtifactResponse;
|
||||||
use codex_hooks::HookEvent;
|
use codex_hooks::HookEvent;
|
||||||
use codex_hooks::HookEventAfterAgent;
|
use codex_hooks::HookEventAfterAgent;
|
||||||
use codex_hooks::HookPayload;
|
use codex_hooks::HookPayload;
|
||||||
@@ -1786,7 +1789,16 @@ impl Session {
|
|||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.presentation_artifacts.execute_requests(request, cwd)
|
state.artifacts.presentation.execute_requests(request, cwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn execute_spreadsheet_artifact(
|
||||||
|
&self,
|
||||||
|
request: SpreadsheetArtifactRequest,
|
||||||
|
cwd: &Path,
|
||||||
|
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
state.artifacts.spreadsheet.execute(request, cwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn record_initial_history(&self, conversation_history: InitialHistory) {
|
async fn record_initial_history(&self, conversation_history: InitialHistory) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Session-wide mutable state.
|
//! Session-wide mutable state.
|
||||||
|
|
||||||
use codex_artifact_presentation::PresentationArtifactManager;
|
use codex_artifact_presentation::PresentationArtifactManager;
|
||||||
|
use codex_artifact_spreadsheet::SpreadsheetArtifactManager;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -17,6 +18,12 @@ use crate::tasks::RegularTask;
|
|||||||
use crate::truncate::TruncationPolicy;
|
use crate::truncate::TruncationPolicy;
|
||||||
use codex_protocol::protocol::TurnContextItem;
|
use codex_protocol::protocol::TurnContextItem;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct SessionArtifacts {
|
||||||
|
pub(crate) presentation: PresentationArtifactManager,
|
||||||
|
pub(crate) spreadsheet: SpreadsheetArtifactManager,
|
||||||
|
}
|
||||||
|
|
||||||
/// Persistent, session-scoped state previously stored directly on `Session`.
|
/// Persistent, session-scoped state previously stored directly on `Session`.
|
||||||
pub(crate) struct SessionState {
|
pub(crate) struct SessionState {
|
||||||
pub(crate) session_configuration: SessionConfiguration,
|
pub(crate) session_configuration: SessionConfiguration,
|
||||||
@@ -33,7 +40,7 @@ pub(crate) struct SessionState {
|
|||||||
pub(crate) startup_regular_task: Option<JoinHandle<CodexResult<RegularTask>>>,
|
pub(crate) startup_regular_task: Option<JoinHandle<CodexResult<RegularTask>>>,
|
||||||
pub(crate) active_mcp_tool_selection: Option<Vec<String>>,
|
pub(crate) active_mcp_tool_selection: Option<Vec<String>>,
|
||||||
pub(crate) active_connector_selection: HashSet<String>,
|
pub(crate) active_connector_selection: HashSet<String>,
|
||||||
pub(crate) presentation_artifacts: PresentationArtifactManager,
|
pub(crate) artifacts: SessionArtifacts,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionState {
|
impl SessionState {
|
||||||
@@ -51,7 +58,7 @@ impl SessionState {
|
|||||||
startup_regular_task: None,
|
startup_regular_task: None,
|
||||||
active_mcp_tool_selection: None,
|
active_mcp_tool_selection: None,
|
||||||
active_connector_selection: HashSet::new(),
|
active_connector_selection: HashSet::new(),
|
||||||
presentation_artifacts: PresentationArtifactManager::default(),
|
artifacts: SessionArtifacts::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ mod read_file;
|
|||||||
mod request_user_input;
|
mod request_user_input;
|
||||||
mod search_tool_bm25;
|
mod search_tool_bm25;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod spreadsheet_artifact;
|
||||||
mod test_sync;
|
mod test_sync;
|
||||||
pub(crate) mod unified_exec;
|
pub(crate) mod unified_exec;
|
||||||
mod view_image;
|
mod view_image;
|
||||||
@@ -48,6 +49,7 @@ pub(crate) use search_tool_bm25::SEARCH_TOOL_BM25_TOOL_NAME;
|
|||||||
pub use search_tool_bm25::SearchToolBm25Handler;
|
pub use search_tool_bm25::SearchToolBm25Handler;
|
||||||
pub use shell::ShellCommandHandler;
|
pub use shell::ShellCommandHandler;
|
||||||
pub use shell::ShellHandler;
|
pub use shell::ShellHandler;
|
||||||
|
pub use spreadsheet_artifact::SpreadsheetArtifactHandler;
|
||||||
pub use test_sync::TestSyncHandler;
|
pub use test_sync::TestSyncHandler;
|
||||||
pub use unified_exec::UnifiedExecHandler;
|
pub use unified_exec::UnifiedExecHandler;
|
||||||
pub use view_image::ViewImageHandler;
|
pub use view_image::ViewImageHandler;
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use codex_artifact_spreadsheet::PathAccessKind;
|
||||||
|
use codex_artifact_spreadsheet::PathAccessRequirement;
|
||||||
|
use codex_artifact_spreadsheet::SpreadsheetArtifactError;
|
||||||
|
use codex_artifact_spreadsheet::SpreadsheetArtifactRequest;
|
||||||
|
use codex_protocol::models::FunctionCallOutputBody;
|
||||||
|
use codex_protocol::protocol::AskForApproval;
|
||||||
|
use codex_protocol::protocol::ReviewDecision;
|
||||||
|
use serde_json::to_string;
|
||||||
|
use std::path::Component;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::codex::Session;
|
||||||
|
use crate::codex::TurnContext;
|
||||||
|
use crate::features::Feature;
|
||||||
|
use crate::function_tool::FunctionCallError;
|
||||||
|
use crate::path_utils::normalize_for_path_comparison;
|
||||||
|
use crate::path_utils::resolve_symlink_write_paths;
|
||||||
|
use crate::tools::context::ToolInvocation;
|
||||||
|
use crate::tools::context::ToolOutput;
|
||||||
|
use crate::tools::context::ToolPayload;
|
||||||
|
use crate::tools::handlers::parse_arguments;
|
||||||
|
use crate::tools::registry::ToolHandler;
|
||||||
|
use crate::tools::registry::ToolKind;
|
||||||
|
use crate::tools::sandboxing::with_cached_approval;
|
||||||
|
|
||||||
|
pub struct SpreadsheetArtifactHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ToolHandler for SpreadsheetArtifactHandler {
|
||||||
|
fn kind(&self) -> ToolKind {
|
||||||
|
ToolKind::Function
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
|
||||||
|
let ToolInvocation {
|
||||||
|
session,
|
||||||
|
turn,
|
||||||
|
payload,
|
||||||
|
call_id,
|
||||||
|
..
|
||||||
|
} = invocation;
|
||||||
|
|
||||||
|
if !session.enabled(Feature::Artifact) {
|
||||||
|
return Err(FunctionCallError::RespondToModel(
|
||||||
|
"spreadsheet_artifact is disabled by feature flag".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let arguments = match payload {
|
||||||
|
ToolPayload::Function { arguments } => arguments,
|
||||||
|
_ => {
|
||||||
|
return Err(FunctionCallError::RespondToModel(
|
||||||
|
"spreadsheet_artifact handler received unsupported payload".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request: SpreadsheetArtifactRequest = parse_arguments(&arguments)?;
|
||||||
|
for access in request
|
||||||
|
.required_path_accesses(&turn.cwd)
|
||||||
|
.map_err(spreadsheet_error)?
|
||||||
|
{
|
||||||
|
authorize_path_access(session.as_ref(), turn.as_ref(), &call_id, &access).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = session
|
||||||
|
.execute_spreadsheet_artifact(request, &turn.cwd)
|
||||||
|
.await
|
||||||
|
.map_err(spreadsheet_error)?;
|
||||||
|
|
||||||
|
Ok(ToolOutput::Function {
|
||||||
|
body: FunctionCallOutputBody::Text(to_string(&response).map_err(|error| {
|
||||||
|
FunctionCallError::RespondToModel(format!(
|
||||||
|
"failed to serialize spreadsheet_artifact response: {error}"
|
||||||
|
))
|
||||||
|
})?),
|
||||||
|
success: Some(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spreadsheet_error(error: SpreadsheetArtifactError) -> FunctionCallError {
|
||||||
|
FunctionCallError::RespondToModel(error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authorize_path_access(
|
||||||
|
session: &Session,
|
||||||
|
turn: &TurnContext,
|
||||||
|
call_id: &str,
|
||||||
|
access: &PathAccessRequirement,
|
||||||
|
) -> Result<(), FunctionCallError> {
|
||||||
|
let effective_path = match access.kind {
|
||||||
|
PathAccessKind::Read => effective_read_path(&access.path),
|
||||||
|
PathAccessKind::Write => effective_write_path(&access.path),
|
||||||
|
};
|
||||||
|
let allowed = match access.kind {
|
||||||
|
PathAccessKind::Read => path_is_readable(turn, &effective_path),
|
||||||
|
PathAccessKind::Write => path_is_writable(turn, &effective_path),
|
||||||
|
};
|
||||||
|
if allowed {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let approval_policy = turn.approval_policy.value();
|
||||||
|
if !matches!(
|
||||||
|
approval_policy,
|
||||||
|
AskForApproval::OnRequest | AskForApproval::UnlessTrusted
|
||||||
|
) {
|
||||||
|
return Err(FunctionCallError::RespondToModel(format!(
|
||||||
|
"{} path `{}` is outside the current sandbox policy",
|
||||||
|
access_kind_label(access.kind),
|
||||||
|
access.path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = format!(
|
||||||
|
"spreadsheet_artifact:{:?}:{}",
|
||||||
|
access.kind,
|
||||||
|
effective_path.display()
|
||||||
|
);
|
||||||
|
let path = access.path.clone();
|
||||||
|
let action = access.action.clone();
|
||||||
|
let decision =
|
||||||
|
with_cached_approval(&session.services, "spreadsheet_artifact", vec![key], || {
|
||||||
|
let path = path.clone();
|
||||||
|
let action = action.clone();
|
||||||
|
async move {
|
||||||
|
session
|
||||||
|
.request_command_approval(
|
||||||
|
turn,
|
||||||
|
call_id.to_string(),
|
||||||
|
None,
|
||||||
|
vec![
|
||||||
|
"spreadsheet_artifact".to_string(),
|
||||||
|
action,
|
||||||
|
path.display().to_string(),
|
||||||
|
],
|
||||||
|
turn.cwd.clone(),
|
||||||
|
Some(format!(
|
||||||
|
"Allow spreadsheet_artifact to {} `{}`?",
|
||||||
|
access_kind_verb(access.kind),
|
||||||
|
path.display()
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
decision,
|
||||||
|
ReviewDecision::Approved
|
||||||
|
| ReviewDecision::ApprovedForSession
|
||||||
|
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||||
|
) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(FunctionCallError::RespondToModel(format!(
|
||||||
|
"{} path `{}` was not approved",
|
||||||
|
access_kind_label(access.kind),
|
||||||
|
access.path.display()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_is_readable(turn: &TurnContext, path: &Path) -> bool {
|
||||||
|
if turn.sandbox_policy.has_full_disk_read_access() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
turn.sandbox_policy
|
||||||
|
.get_readable_roots_with_cwd(&turn.cwd)
|
||||||
|
.iter()
|
||||||
|
.any(|root| path.starts_with(root.as_path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_is_writable(turn: &TurnContext, path: &Path) -> bool {
|
||||||
|
if turn.sandbox_policy.has_full_disk_write_access() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
turn.sandbox_policy
|
||||||
|
.get_writable_roots_with_cwd(&turn.cwd)
|
||||||
|
.iter()
|
||||||
|
.any(|root| root.is_path_writable(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_read_path(path: &Path) -> PathBuf {
|
||||||
|
normalize_for_path_comparison(path).unwrap_or_else(|_| normalize_without_fs(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_write_path(path: &Path) -> PathBuf {
|
||||||
|
let write_path = resolve_symlink_write_paths(path)
|
||||||
|
.map(|paths| paths.write_path)
|
||||||
|
.unwrap_or_else(|_| path.to_path_buf());
|
||||||
|
normalize_for_path_comparison(&write_path).unwrap_or_else(|_| normalize_without_fs(&write_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_without_fs(path: &Path) -> PathBuf {
|
||||||
|
let mut normalized = PathBuf::new();
|
||||||
|
for component in path.components() {
|
||||||
|
match component {
|
||||||
|
Component::ParentDir => {
|
||||||
|
normalized.pop();
|
||||||
|
}
|
||||||
|
Component::CurDir => {}
|
||||||
|
other => normalized.push(other.as_os_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
fn access_kind_label(kind: PathAccessKind) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
PathAccessKind::Read => "read",
|
||||||
|
PathAccessKind::Write => "write",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn access_kind_verb(kind: PathAccessKind) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
PathAccessKind::Read => "read from",
|
||||||
|
PathAccessKind::Write => "write to",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,8 +35,6 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
const SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE: &str =
|
const SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE: &str =
|
||||||
include_str!("../../templates/search_tool/tool_description.md");
|
include_str!("../../templates/search_tool/tool_description.md");
|
||||||
const PRESENTATION_ARTIFACT_DESCRIPTION_TEMPLATE: &str =
|
|
||||||
include_str!("../../templates/tools/presentation_artifact.md");
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum ShellCommandBackendConfig {
|
pub enum ShellCommandBackendConfig {
|
||||||
@@ -57,7 +55,7 @@ pub(crate) struct ToolsConfig {
|
|||||||
pub js_repl_enabled: bool,
|
pub js_repl_enabled: bool,
|
||||||
pub js_repl_tools_only: bool,
|
pub js_repl_tools_only: bool,
|
||||||
pub collab_tools: bool,
|
pub collab_tools: bool,
|
||||||
pub presentation_artifact: bool,
|
pub artifact_tools: bool,
|
||||||
pub default_mode_request_user_input: bool,
|
pub default_mode_request_user_input: bool,
|
||||||
pub experimental_supported_tools: Vec<String>,
|
pub experimental_supported_tools: Vec<String>,
|
||||||
pub agent_jobs_tools: bool,
|
pub agent_jobs_tools: bool,
|
||||||
@@ -87,7 +85,7 @@ impl ToolsConfig {
|
|||||||
let include_default_mode_request_user_input =
|
let include_default_mode_request_user_input =
|
||||||
features.enabled(Feature::DefaultModeRequestUserInput);
|
features.enabled(Feature::DefaultModeRequestUserInput);
|
||||||
let include_search_tool = features.enabled(Feature::Apps);
|
let include_search_tool = features.enabled(Feature::Apps);
|
||||||
let include_presentation_artifact = features.enabled(Feature::Artifact);
|
let include_artifact_tools = features.enabled(Feature::Artifact);
|
||||||
let include_agent_jobs = include_collab_tools && features.enabled(Feature::Sqlite);
|
let include_agent_jobs = include_collab_tools && features.enabled(Feature::Sqlite);
|
||||||
let request_permission_enabled = features.enabled(Feature::RequestPermissions);
|
let request_permission_enabled = features.enabled(Feature::RequestPermissions);
|
||||||
let shell_command_backend =
|
let shell_command_backend =
|
||||||
@@ -143,7 +141,7 @@ impl ToolsConfig {
|
|||||||
js_repl_enabled: include_js_repl,
|
js_repl_enabled: include_js_repl,
|
||||||
js_repl_tools_only: include_js_repl_tools_only,
|
js_repl_tools_only: include_js_repl_tools_only,
|
||||||
collab_tools: include_collab_tools,
|
collab_tools: include_collab_tools,
|
||||||
presentation_artifact: include_presentation_artifact,
|
artifact_tools: include_artifact_tools,
|
||||||
default_mode_request_user_input: include_default_mode_request_user_input,
|
default_mode_request_user_input: include_default_mode_request_user_input,
|
||||||
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
||||||
agent_jobs_tools: include_agent_jobs,
|
agent_jobs_tools: include_agent_jobs,
|
||||||
@@ -609,7 +607,7 @@ fn create_presentation_artifact_tool() -> ToolSpec {
|
|||||||
|
|
||||||
ToolSpec::Function(ResponsesApiTool {
|
ToolSpec::Function(ResponsesApiTool {
|
||||||
name: "presentation_artifact".to_string(),
|
name: "presentation_artifact".to_string(),
|
||||||
description: PRESENTATION_ARTIFACT_DESCRIPTION_TEMPLATE.to_string(),
|
description: "Create or edit a presentation artifact for the current thread.".to_string(),
|
||||||
strict: false,
|
strict: false,
|
||||||
parameters: JsonSchema::Object {
|
parameters: JsonSchema::Object {
|
||||||
properties,
|
properties,
|
||||||
@@ -619,6 +617,44 @@ fn create_presentation_artifact_tool() -> ToolSpec {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_spreadsheet_artifact_tool() -> ToolSpec {
|
||||||
|
let properties = BTreeMap::from([
|
||||||
|
(
|
||||||
|
"artifact_id".to_string(),
|
||||||
|
JsonSchema::String {
|
||||||
|
description: Some(
|
||||||
|
"Artifact id returned by an earlier spreadsheet_artifact call.".to_string(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"action".to_string(),
|
||||||
|
JsonSchema::String {
|
||||||
|
description: Some("Action name to run for this request.".to_string()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"args".to_string(),
|
||||||
|
JsonSchema::Object {
|
||||||
|
properties: BTreeMap::new(),
|
||||||
|
required: None,
|
||||||
|
additional_properties: Some(true.into()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ToolSpec::Function(ResponsesApiTool {
|
||||||
|
name: "spreadsheet_artifact".to_string(),
|
||||||
|
description: "Create or edit a spreadsheet artifact for the current thread.".to_string(),
|
||||||
|
strict: false,
|
||||||
|
parameters: JsonSchema::Object {
|
||||||
|
properties,
|
||||||
|
required: Some(vec!["action".to_string(), "args".to_string()]),
|
||||||
|
additional_properties: Some(false.into()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn create_collab_input_items_schema() -> JsonSchema {
|
fn create_collab_input_items_schema() -> JsonSchema {
|
||||||
let properties = BTreeMap::from([
|
let properties = BTreeMap::from([
|
||||||
(
|
(
|
||||||
@@ -1722,6 +1758,7 @@ pub(crate) fn build_specs(
|
|||||||
use crate::tools::handlers::SearchToolBm25Handler;
|
use crate::tools::handlers::SearchToolBm25Handler;
|
||||||
use crate::tools::handlers::ShellCommandHandler;
|
use crate::tools::handlers::ShellCommandHandler;
|
||||||
use crate::tools::handlers::ShellHandler;
|
use crate::tools::handlers::ShellHandler;
|
||||||
|
use crate::tools::handlers::SpreadsheetArtifactHandler;
|
||||||
use crate::tools::handlers::TestSyncHandler;
|
use crate::tools::handlers::TestSyncHandler;
|
||||||
use crate::tools::handlers::UnifiedExecHandler;
|
use crate::tools::handlers::UnifiedExecHandler;
|
||||||
use crate::tools::handlers::ViewImageHandler;
|
use crate::tools::handlers::ViewImageHandler;
|
||||||
@@ -1745,6 +1782,7 @@ pub(crate) fn build_specs(
|
|||||||
let js_repl_handler = Arc::new(JsReplHandler);
|
let js_repl_handler = Arc::new(JsReplHandler);
|
||||||
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
|
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
|
||||||
let presentation_artifact_handler = Arc::new(PresentationArtifactHandler);
|
let presentation_artifact_handler = Arc::new(PresentationArtifactHandler);
|
||||||
|
let spreadsheet_artifact_handler = Arc::new(SpreadsheetArtifactHandler);
|
||||||
let request_permission_enabled = config.request_permission_enabled;
|
let request_permission_enabled = config.request_permission_enabled;
|
||||||
|
|
||||||
match &config.shell_type {
|
match &config.shell_type {
|
||||||
@@ -1882,9 +1920,11 @@ pub(crate) fn build_specs(
|
|||||||
builder.push_spec_with_parallel_support(create_view_image_tool(), true);
|
builder.push_spec_with_parallel_support(create_view_image_tool(), true);
|
||||||
builder.register_handler("view_image", view_image_handler);
|
builder.register_handler("view_image", view_image_handler);
|
||||||
|
|
||||||
if config.presentation_artifact {
|
if config.artifact_tools {
|
||||||
builder.push_spec(create_presentation_artifact_tool());
|
builder.push_spec(create_presentation_artifact_tool());
|
||||||
|
builder.push_spec(create_spreadsheet_artifact_tool());
|
||||||
builder.register_handler("presentation_artifact", presentation_artifact_handler);
|
builder.register_handler("presentation_artifact", presentation_artifact_handler);
|
||||||
|
builder.register_handler("spreadsheet_artifact", spreadsheet_artifact_handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.collab_tools {
|
if config.collab_tools {
|
||||||
@@ -2201,7 +2241,7 @@ mod tests {
|
|||||||
session_source: SessionSource::Cli,
|
session_source: SessionSource::Cli,
|
||||||
});
|
});
|
||||||
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
||||||
assert_contains_tool_names(&tools, &["presentation_artifact"]);
|
assert_contains_tool_names(&tools, &["presentation_artifact", "spreadsheet_artifact"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user