"""content_publish tool — publish a session file for inline viewing in chat.
The file must already exist in the session's file directory.
Publishing only registers metadata; the file itself is NOT copied.
The user sees the file via the existing /api/sessions/{id}/files/{name} endpoint,
so edits made by the agent are immediately visible.
"""
from pathlib import Path
from navi.content_store import publish
from navi.config import settings
from navi.session_files import session_dir
from ._internal.base import Tool, ToolContext, ToolResult, current_session_id
class ContentPublishTool(Tool):
name = "content_publish"
description = (
"Publish a file from this session's directory for inline viewing in the chat — HTML, "
"SVG, PDF, images, video, STL models — as an interactive card.\n\n"
"The file MUST already be in the session directory: this tool registers it and does "
"NOT copy from workspace/. Write, copy or move it there first. Editing it afterwards "
"updates the card in place, so publish once.\n\n"
"A file kept elsewhere that only needs a download link is share_file's job.\n\n"
'Filenames, re-publishing, STL sources: tool_manual("content_publish").'
)
parameters = {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": (
"Name of the file to publish. "
"The file must already exist in the session directory. "
"Example: 'chart.svg' or 'report.html'."
),
},
"title": {
"type": "string",
"description": "Human-readable title shown on the content card",
},
"content_type": {
"type": "string",
"enum": ["stl", "html", "svg", "pdf", "image", "video", "unknown"],
"description": "Content type for viewer selection. Auto-detected from extension if omitted.",
},
"source_filename": {
"type": "string",
"description": (
"Optional source file in the same session directory. "
"Use this mainly for STL models when an OpenSCAD .scad source file exists. "
"Do not invent it; omit it if the model was downloaded or no source file exists."
),
},
},
"required": ["filename"],
}
async def execute(self, params: dict, ctx: ToolContext | None = None) -> ToolResult:
session_id = ctx.session_id if ctx else current_session_id.get()
if not session_id:
return ToolResult(
success=False,
output="No active session context.",
error="no_session",
)
filename = Path(params["filename"]).name # strip any path components
sess_dir = session_dir(session_id)
src = sess_dir / filename
if not src.exists():
return ToolResult(
success=False,
output=(
f"File '{filename}' not found in the session directory: {sess_dir}\n"
f"SESSION_FILES_DIR is configured as: {settings.session_files_dir}\n"
f"Make sure the file was written, copied, or moved there before publishing. "
f"`workspace/` is separate and is not publishable directly. "
f"You can check the directory contents with `filesystem list {sess_dir}`."
),
error="not_found",
)
if not src.is_file():
return ToolResult(
success=False,
output=f"Path is not a file: {src}",
error="not_a_file",
)
try:
info = await publish(
session_id=session_id,
filename=filename,
title=params.get("title"),
content_type=params.get("content_type"),
source_filename=params.get("source_filename"),
)
except FileNotFoundError as e:
return ToolResult(success=False, output=str(e), error="not_found")
except IsADirectoryError as e:
return ToolResult(success=False, output=str(e), error="is_directory")
except Exception as e:
return ToolResult(success=False, output=f"Publish failed: {e}", error="publish_failed")
return ToolResult(
success=True,
output=(
f"Published: {info['title']} ({info['content_type']})\n"
f"URL: {info['url']}\n"
f"ID: {info['id']}\n"
+ (f"Source: {info['source_filename']}\n" if info.get("source_filename") else "")
+ f"If you need to edit this file later, edit it at: {src}"
),
metadata=info,
)