feat(chat): Files in this chat — persistent per-chat artifact panel (#3024) (#3026)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
oxoxDev
2026-06-02 17:22:08 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent 00b8a669c6
commit 71e04eac45
36 changed files with 2982 additions and 389 deletions
+11
View File
@@ -368,6 +368,14 @@ pub enum DomainEvent {
kind: String,
/// Human-readable title (also the on-disk filename stem).
title: String,
/// Absolute workspace root the artifact belongs to (matches
/// the `workspace_dir` parameter passed to
/// `finalize_artifact`). Bound to the event so a subscriber
/// firing AFTER the user switched workspaces can detect the
/// mismatch and drop the surface — `path` is workspace-
/// relative and would otherwise resolve into the wrong
/// `<workspace>/artifacts/` tree.
workspace_dir: String,
/// Relative path under `<workspace>/artifacts/`, e.g.
/// `"<uuid>/deck.pptx"`. The absolute path is reachable via
/// `ai_get_artifact` so the renderer never needs the
@@ -391,6 +399,9 @@ pub enum DomainEvent {
artifact_id: String,
kind: String,
title: String,
/// Absolute workspace root the artifact belongs to — see
/// [`Self::ArtifactReady::workspace_dir`] for rationale.
workspace_dir: String,
/// Producer-supplied failure reason. Already truncated by the
/// producer (e.g. `PresentationError::truncate_stderr`).
error: String,
+1
View File
@@ -2075,6 +2075,7 @@ pub async fn bootstrap_core_runtime(embedded_core: bool) {
// case. Without this, the gate parks and publishes but nothing reaches the
// frontend → every prompt dies at the TTL. Idempotent (Once-guarded).
crate::openhuman::channels::providers::web::register_approval_surface_subscriber();
crate::openhuman::channels::providers::web::register_artifact_surface_subscriber();
} else {
log::info!(
"[runtime] approval gate disabled (OPENHUMAN_APPROVAL_GATE=0) — \
+3 -5
View File
@@ -364,11 +364,7 @@ pub async fn finalize_artifact(
size_bytes: u64,
) -> Result<ArtifactMeta, String> {
let mut meta = get_artifact(workspace_dir, artifact_id).await?;
if matches!(meta.status, ArtifactStatus::Ready) {
// Idempotent on status alone: a second finalize with a different
// size_bytes shouldn't re-emit ArtifactReady and flap the UI card
// — once an artifact is Ready, callers should not be redefining
// its size. Per graycyrus on PR #3017.
if matches!(meta.status, ArtifactStatus::Ready) && meta.size_bytes == size_bytes {
log::debug!("[artifacts] finalize_artifact: id={artifact_id} already Ready, no-op");
return Ok(meta);
}
@@ -383,6 +379,7 @@ pub async fn finalize_artifact(
artifact_id: meta.id.clone(),
kind: meta.kind.as_str().to_string(),
title: meta.title.clone(),
workspace_dir: workspace_dir.to_string_lossy().into_owned(),
path: meta.path.clone(),
size_bytes: meta.size_bytes,
thread_id,
@@ -422,6 +419,7 @@ pub async fn fail_artifact(
artifact_id: meta.id.clone(),
kind: meta.kind.as_str().to_string(),
title: meta.title.clone(),
workspace_dir: workspace_dir.to_string_lossy().into_owned(),
error: reason.to_string(),
thread_id,
client_id,
+4
View File
@@ -112,6 +112,7 @@ impl EventHandler for ArtifactSurfaceSubscriber {
artifact_id,
kind,
title,
workspace_dir,
path,
size_bytes,
thread_id,
@@ -134,6 +135,7 @@ impl EventHandler for ArtifactSurfaceSubscriber {
"artifact_id": artifact_id,
"kind": kind,
"title": title,
"workspace_dir": workspace_dir,
"path": path,
"size_bytes": size_bytes,
})),
@@ -144,6 +146,7 @@ impl EventHandler for ArtifactSurfaceSubscriber {
artifact_id,
kind,
title,
workspace_dir,
error,
thread_id,
client_id,
@@ -166,6 +169,7 @@ impl EventHandler for ArtifactSurfaceSubscriber {
"artifact_id": artifact_id,
"kind": kind,
"title": title,
"workspace_dir": workspace_dir,
"error": error,
})),
..Default::default()