mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Merge remote-tracking branch 'origin/main' into moim2
* origin/main: Art vandelay: Import & Export (#5053) docs: misc updates for extensions directory (#5035) updating recipe scanner workflows for detecting recipes from forked repos (#5056) feat(prompt-library): add Smart Meeting Assistant advanced prompt (#4998) (#5031) Allow auto focus and typing while chat is initializing (#5043) docs(blog): Add blog for running Goose in containerized envs (#5052)
This commit is contained in:
@@ -43,9 +43,9 @@ jobs:
|
||||
echo "📝 Synchronize event - checking files changed since previous commit"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }})
|
||||
else
|
||||
# For opened/reopened, check all files in the PR
|
||||
# For opened/reopened, check all files in the PR (compare PR head against base)
|
||||
echo "📝 PR opened/reopened - checking all files in PR"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}..HEAD)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
||||
fi
|
||||
|
||||
echo "Changed files in this push:"
|
||||
@@ -78,9 +78,9 @@ jobs:
|
||||
echo "📝 Synchronize event - checking files changed/added since previous commit"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }})
|
||||
else
|
||||
# For opened/reopened, check all files in the PR (new and modified)
|
||||
# For opened/reopened, check all files in the PR (compare PR head against base)
|
||||
echo "📝 PR opened/reopened - checking all new/modified files in PR"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}..HEAD)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
||||
fi
|
||||
|
||||
# Filter for recipe files only that were changed or added
|
||||
|
||||
@@ -24,15 +24,20 @@ jobs:
|
||||
set -e
|
||||
echo "🔍 Checking if recipe files were added or modified in merged PR..."
|
||||
|
||||
# Get the current commit (merge commit) and the previous commit on the base branch
|
||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||
PREVIOUS_COMMIT=$(git rev-parse HEAD~1)
|
||||
# Get the PR merge information
|
||||
MERGE_COMMIT=$(git rev-parse HEAD)
|
||||
echo "Merge commit: $MERGE_COMMIT"
|
||||
|
||||
echo "Current commit: $CURRENT_COMMIT"
|
||||
echo "Previous commit: $PREVIOUS_COMMIT"
|
||||
# For merged PRs, compare the PR's changes against the base branch
|
||||
# Use the PR information from the event to get the actual changes
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
# Get the list of files that were added or modified in this merge (not deleted)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM $PREVIOUS_COMMIT..$CURRENT_COMMIT)
|
||||
echo "PR base SHA: $BASE_SHA"
|
||||
echo "PR head SHA: $HEAD_SHA"
|
||||
|
||||
# Get the list of files that were added or modified in the PR (not deleted)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM $BASE_SHA..$HEAD_SHA)
|
||||
|
||||
echo "Files added/modified in merged PR:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
@@ -56,9 +56,9 @@ jobs:
|
||||
echo "📝 Synchronize event - checking files changed since previous commit"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }})
|
||||
else
|
||||
# For opened/reopened, check all files in the PR
|
||||
# For opened/reopened, check all files in the PR (compare PR head against base)
|
||||
echo "📝 PR opened/reopened - checking all files in PR"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}..HEAD)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
||||
fi
|
||||
|
||||
echo "Changed files in this push:"
|
||||
@@ -87,13 +87,13 @@ jobs:
|
||||
echo "📝 Synchronize event - checking files changed/added since previous commit"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }})
|
||||
else
|
||||
# For opened/reopened, check all files in the PR (new and modified)
|
||||
# For opened/reopened, check all files in the PR (compare PR head against base)
|
||||
echo "📝 PR opened/reopened - checking all new/modified files in PR"
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}..HEAD)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
||||
fi
|
||||
|
||||
# Filter for recipe files only that were changed or added
|
||||
RECIPE_FILES=$(git diff --diff-filter=AM --name-only origin/${{ github.base_ref }}..HEAD | grep "^documentation/src/pages/recipes/data/recipes/" | grep -E "\.(yaml|yml)$" || true)
|
||||
RECIPE_FILES=$(echo "$CHANGED_FILES" | grep "^documentation/src/pages/recipes/data/recipes/" | grep -E "\.(yaml|yml)$" || true)
|
||||
|
||||
if [ -z "$RECIPE_FILES" ]; then
|
||||
echo "No changed recipe files found in PR"
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
# Check if this is a new file or an update to existing file
|
||||
# Get list of changed files in this PR compared to base branch
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.event.pull_request.base.ref }}...HEAD | grep "^$RECIPE_FILE$" || true)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep "^$RECIPE_FILE$" || true)
|
||||
EXISTING_FILES=$(find documentation/src/pages/recipes/data/recipes/ -name "$FILENAME.yaml" -o -name "$FILENAME.yml" | grep -v "^$RECIPE_FILE$" || true)
|
||||
|
||||
if [ -n "$EXISTING_FILES" ] && [ -z "$CHANGED_FILES" ]; then
|
||||
|
||||
@@ -352,6 +352,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::session::get_session_insights,
|
||||
super::routes::session::update_session_description,
|
||||
super::routes::session::delete_session,
|
||||
super::routes::session::export_session,
|
||||
super::routes::session::import_session,
|
||||
super::routes::session::update_session_user_recipe_values,
|
||||
super::routes::schedule::create_schedule,
|
||||
super::routes::schedule::list_schedules,
|
||||
@@ -389,6 +391,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::reply::ChatRequest,
|
||||
super::routes::context::ContextManageRequest,
|
||||
super::routes::context::ContextManageResponse,
|
||||
super::routes::session::ImportSessionRequest,
|
||||
super::routes::session::SessionListResponse,
|
||||
super::routes::session::UpdateSessionDescriptionRequest,
|
||||
super::routes::session::UpdateSessionUserRecipeValuesRequest,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::state::AppState;
|
||||
use axum::routing::post;
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
@@ -33,6 +34,12 @@ pub struct UpdateSessionUserRecipeValuesRequest {
|
||||
user_recipe_values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportSessionRequest {
|
||||
json: String,
|
||||
}
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH: usize = 200;
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -199,11 +206,63 @@ async fn delete_session(Path(session_id): Path<String>) -> Result<StatusCode, St
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/sessions/{session_id}/export",
|
||||
params(
|
||||
("session_id" = String, Path, description = "Unique identifier for the session")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Session exported successfully", body = String),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||
(status = 404, description = "Session not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
tag = "Session Management"
|
||||
)]
|
||||
async fn export_session(Path(session_id): Path<String>) -> Result<Json<String>, StatusCode> {
|
||||
let exported = SessionManager::export_session(&session_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
Ok(Json(exported))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/sessions/import",
|
||||
request_body = ImportSessionRequest,
|
||||
responses(
|
||||
(status = 200, description = "Session imported successfully", body = Session),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||
(status = 400, description = "Bad request - Invalid JSON"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
tag = "Session Management"
|
||||
)]
|
||||
async fn import_session(
|
||||
Json(request): Json<ImportSessionRequest>,
|
||||
) -> Result<Json<Session>, StatusCode> {
|
||||
let session = SessionManager::import_session(&request.json)
|
||||
.await
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
|
||||
Ok(Json(session))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/sessions", get(list_sessions))
|
||||
.route("/sessions/{session_id}", get(get_session))
|
||||
.route("/sessions/{session_id}", delete(delete_session))
|
||||
.route("/sessions/{session_id}/export", get(export_session))
|
||||
.route("/sessions/import", post(import_session))
|
||||
.route("/sessions/insights", get(get_session_insights))
|
||||
.route(
|
||||
"/sessions/{session_id}/description",
|
||||
|
||||
@@ -211,6 +211,14 @@ impl SessionManager {
|
||||
Self::instance().await?.get_insights().await
|
||||
}
|
||||
|
||||
pub async fn export_session(id: &str) -> Result<String> {
|
||||
Self::instance().await?.export_session(id).await
|
||||
}
|
||||
|
||||
pub async fn import_session(json: &str) -> Result<Session> {
|
||||
Self::instance().await?.import_session(json).await
|
||||
}
|
||||
|
||||
pub async fn maybe_update_description(id: &str, provider: Arc<dyn Provider>) -> Result<()> {
|
||||
let session = Self::get_session(id, true).await?;
|
||||
let conversation = session
|
||||
@@ -895,6 +903,41 @@ impl SessionStorage {
|
||||
total_tokens: row.1.unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
||||
async fn export_session(&self, id: &str) -> Result<String> {
|
||||
let session = self.get_session(id, true).await?;
|
||||
serde_json::to_string_pretty(&session).map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn import_session(&self, json: &str) -> Result<Session> {
|
||||
let import: Session = serde_json::from_str(json)?;
|
||||
|
||||
let session = self
|
||||
.create_session(import.working_dir.clone(), import.description.clone())
|
||||
.await?;
|
||||
|
||||
self.apply_update(
|
||||
SessionUpdateBuilder::new(session.id.clone())
|
||||
.extension_data(import.extension_data)
|
||||
.total_tokens(import.total_tokens)
|
||||
.input_tokens(import.input_tokens)
|
||||
.output_tokens(import.output_tokens)
|
||||
.accumulated_total_tokens(import.accumulated_total_tokens)
|
||||
.accumulated_input_tokens(import.accumulated_input_tokens)
|
||||
.accumulated_output_tokens(import.accumulated_output_tokens)
|
||||
.schedule_id(import.schedule_id)
|
||||
.recipe(import.recipe)
|
||||
.user_recipe_values(import.user_recipe_values),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(conversation) = import.conversation {
|
||||
self.replace_conversation(&session.id, &conversation)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.get_session(&session.id, true).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -997,4 +1040,80 @@ mod tests {
|
||||
let expected_tokens = 100 * NUM_CONCURRENT_SESSIONS * (NUM_CONCURRENT_SESSIONS - 1) / 2;
|
||||
assert_eq!(insights.total_tokens, expected_tokens as i64);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_export_import_roundtrip() {
|
||||
const DESCRIPTION: &str = "Original session";
|
||||
const TOTAL_TOKENS: i32 = 500;
|
||||
const INPUT_TOKENS: i32 = 300;
|
||||
const OUTPUT_TOKENS: i32 = 200;
|
||||
const ACCUMULATED_TOKENS: i32 = 1000;
|
||||
const USER_MESSAGE: &str = "test message";
|
||||
const ASSISTANT_MESSAGE: &str = "test response";
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let db_path = temp_dir.path().join("test_export.db");
|
||||
let storage = Arc::new(SessionStorage::create(&db_path).await.unwrap());
|
||||
|
||||
let original = storage
|
||||
.create_session(PathBuf::from("/tmp/test"), DESCRIPTION.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
storage
|
||||
.apply_update(
|
||||
SessionUpdateBuilder::new(original.id.clone())
|
||||
.total_tokens(Some(TOTAL_TOKENS))
|
||||
.input_tokens(Some(INPUT_TOKENS))
|
||||
.output_tokens(Some(OUTPUT_TOKENS))
|
||||
.accumulated_total_tokens(Some(ACCUMULATED_TOKENS)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
storage
|
||||
.add_message(
|
||||
&original.id,
|
||||
&Message {
|
||||
id: None,
|
||||
role: Role::User,
|
||||
created: chrono::Utc::now().timestamp_millis(),
|
||||
content: vec![MessageContent::text(USER_MESSAGE)],
|
||||
metadata: Default::default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
storage
|
||||
.add_message(
|
||||
&original.id,
|
||||
&Message {
|
||||
id: None,
|
||||
role: Role::Assistant,
|
||||
created: chrono::Utc::now().timestamp_millis(),
|
||||
content: vec![MessageContent::text(ASSISTANT_MESSAGE)],
|
||||
metadata: Default::default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let exported = storage.export_session(&original.id).await.unwrap();
|
||||
let imported = storage.import_session(&exported).await.unwrap();
|
||||
|
||||
assert_ne!(imported.id, original.id);
|
||||
assert_eq!(imported.description, DESCRIPTION);
|
||||
assert_eq!(imported.working_dir, PathBuf::from("/tmp/test"));
|
||||
assert_eq!(imported.total_tokens, Some(TOTAL_TOKENS));
|
||||
assert_eq!(imported.input_tokens, Some(INPUT_TOKENS));
|
||||
assert_eq!(imported.output_tokens, Some(OUTPUT_TOKENS));
|
||||
assert_eq!(imported.accumulated_total_tokens, Some(ACCUMULATED_TOKENS));
|
||||
assert_eq!(imported.message_count, 2);
|
||||
|
||||
let conversation = imported.conversation.unwrap();
|
||||
assert_eq!(conversation.messages().len(), 2);
|
||||
assert_eq!(conversation.messages()[0].role, Role::User);
|
||||
assert_eq!(conversation.messages()[1].role, Role::Assistant);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import GooseDesktopInstaller from '@site/src/components/GooseDesktopInstaller';
|
||||
<YouTubeShortEmbed videoUrl="https://www.youtube.com/embed/1Z8XtjQ9El0" />
|
||||
|
||||
|
||||
This tutorial covers how to add the [ElevenLabs MCP Server](https://github.com/yamadashy/repomix) as a Goose extension to enable AI-powered voice generation, voice cloning, audio editing, and speech-to-text transcription.
|
||||
This tutorial covers how to add the [ElevenLabs MCP Server](https://github.com/elevenlabs/elevenlabs-mcp) as a Goose extension to enable AI-powered voice generation, voice cloning, audio editing, and speech-to-text transcription.
|
||||
|
||||
:::tip TLDR
|
||||
<Tabs groupId="interface">
|
||||
|
||||
@@ -85,7 +85,7 @@ export function ServerCard({ server }: { server: MCPServer }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!server.is_builtin && (
|
||||
{!server.is_builtin && (server.show_install_command !== false) && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsCommandVisible(!isCommandVisible)}
|
||||
@@ -139,7 +139,7 @@ export function ServerCard({ server }: { server: MCPServer }) {
|
||||
>
|
||||
Built-in
|
||||
</div>
|
||||
) : (
|
||||
) : server.show_install_link !== false ? (
|
||||
<a
|
||||
href={getGooseInstallLink(server)}
|
||||
target="_blank"
|
||||
@@ -149,7 +149,7 @@ export function ServerCard({ server }: { server: MCPServer }) {
|
||||
<span>Install</span>
|
||||
<Download className="h-4 w-4" />
|
||||
</a>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,19 @@
|
||||
"submittedDate": "2025-09-26",
|
||||
"tags": ["hacktoberfest", "open-source", "contributing"],
|
||||
"hacktoberfest": true
|
||||
},
|
||||
{
|
||||
"title": "Running Goose in Containers (Without Losing Your Mind)",
|
||||
"author": {
|
||||
"name": "Rupam Golui",
|
||||
"handle": "Itz-Agasta"
|
||||
},
|
||||
"type": "blog",
|
||||
"url": "https://dev.to/agasta/running-goose-in-containers-without-losing-your-mind-3m8",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1759560397953-8221761119ec?w=400&h=225&fit=crop&crop=entropy&auto=format",
|
||||
"submittedDate": "2025-10-04",
|
||||
"tags": ["hacktoberfest"],
|
||||
"hacktoberfest": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ const getDocumentationPath = (serverId: string): string => {
|
||||
Can be enabled on the Extensions page in Goose
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
) : server.show_install_command !== false ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-textStandard">
|
||||
<Terminal className="h-4 w-4" />
|
||||
@@ -129,7 +129,7 @@ const getDocumentationPath = (serverId: string): string => {
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{server.environmentVariables && (
|
||||
@@ -215,7 +215,7 @@ const getDocumentationPath = (serverId: string): string => {
|
||||
>
|
||||
Built-in
|
||||
</div>
|
||||
) : (
|
||||
) : server.show_install_link !== false ? (
|
||||
<a
|
||||
href={getGooseInstallLink(server)}
|
||||
target="_blank"
|
||||
@@ -225,7 +225,7 @@ const getDocumentationPath = (serverId: string): string => {
|
||||
<span>Install</span>
|
||||
<Download className="h-4 w-4" />
|
||||
</a>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -9,6 +9,8 @@ export interface MCPServer {
|
||||
installation_notes: string;
|
||||
is_builtin: boolean;
|
||||
endorsed: boolean;
|
||||
show_install_link?: boolean;
|
||||
show_install_command?: boolean;
|
||||
environmentVariables: {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
"id": "alby-mcp",
|
||||
"name": "Alby",
|
||||
"description": "Bitcoin Lightning Network integration",
|
||||
"command": "npx -y @getalby/alby-mcp-server",
|
||||
"command": "npx -y @getalby/mcp",
|
||||
"link": "https://github.com/getalby/mcp",
|
||||
"installation_notes": "Install using npx package manager.",
|
||||
"is_builtin": false,
|
||||
"endorsed": false,
|
||||
"environmentVariables": [
|
||||
{
|
||||
"name": "ALBY_API_TOKEN",
|
||||
"description": "Required environment variable",
|
||||
"name": "NWC_CONNECTION_STRING",
|
||||
"description": "NWC connection secret from your lightning wallet",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
@@ -77,7 +77,7 @@
|
||||
"name": "Brave Search",
|
||||
"description": "Web search capabilities powered by Brave",
|
||||
"command": "npx -y @brave/brave-search-mcp",
|
||||
"link": "https://github.com/brave/brave-search-mcp",
|
||||
"link": "https://github.com/brave/brave-search-mcp-server",
|
||||
"installation_notes": "Install using npx package manager.",
|
||||
"is_builtin": false,
|
||||
"endorsed": true,
|
||||
@@ -217,7 +217,7 @@
|
||||
"environmentVariables": []
|
||||
},
|
||||
{
|
||||
"id": "dev-to",
|
||||
"id": "dev.to",
|
||||
"name": "Dev.to",
|
||||
"description": "Access Dev.to articles and content (requires local server setup)",
|
||||
"url": "http://localhost:3000/mcp",
|
||||
@@ -232,9 +232,9 @@
|
||||
"id": "elevenlabs-mcp",
|
||||
"name": "ElevenLabs",
|
||||
"description": "Text-to-speech and voice synthesis",
|
||||
"command": "npx -y elevenlabs-mcp",
|
||||
"command": "uvx elevenlabs-mcp",
|
||||
"link": "https://github.com/elevenlabs/elevenlabs-mcp",
|
||||
"installation_notes": "Install using npx package manager.",
|
||||
"installation_notes": "Install using uvx package manager.",
|
||||
"is_builtin": false,
|
||||
"endorsed": true,
|
||||
"environmentVariables": [
|
||||
@@ -269,12 +269,12 @@
|
||||
"environmentVariables": []
|
||||
},
|
||||
{
|
||||
"id": "filesystem-mcp",
|
||||
"id": "filesystem",
|
||||
"name": "Filesystem",
|
||||
"description": "File system operations and management",
|
||||
"command": "uvx mcp-server-filesystem",
|
||||
"command": "npx -y @modelcontextprotocol/server-filesystem /path/to/dir1 /path/to/dir2",
|
||||
"link": "https://github.com/modelcontextprotocol/servers",
|
||||
"installation_notes": "Install using uvx package manager.",
|
||||
"installation_notes": "Install using npx package manager.",
|
||||
"is_builtin": false,
|
||||
"endorsed": true,
|
||||
"environmentVariables": []
|
||||
@@ -446,7 +446,7 @@
|
||||
"id": "netlify-mcp",
|
||||
"name": "Netlify",
|
||||
"description": "Web deployment and hosting automation",
|
||||
"command": "npx -y netlify-mcp",
|
||||
"command": "npx -y @netlify/mcp",
|
||||
"link": "https://github.com/netlify/netlify-mcp",
|
||||
"installation_notes": "Install using npx package manager.",
|
||||
"is_builtin": false,
|
||||
@@ -463,8 +463,8 @@
|
||||
"id": "nostrbook-mcp",
|
||||
"name": "NostrBook",
|
||||
"description": "Nostr protocol integration for decentralized social",
|
||||
"command": "npx -y nostrbook-mcp",
|
||||
"link": "https://github.com/nostrbook/nostrbook-mcp",
|
||||
"command": "npx -y @nostrbook/mcp@latest",
|
||||
"link": "https://gitlab.com/soapbox-pub/nostrbook",
|
||||
"installation_notes": "Install using npx package manager.",
|
||||
"is_builtin": false,
|
||||
"endorsed": false,
|
||||
@@ -644,7 +644,7 @@
|
||||
"description": "Web search capabilities powered by Tavily",
|
||||
"command": "npx -y tavily-mcp",
|
||||
"link": "https://github.com/tavily-ai/tavily-mcp",
|
||||
"installation_notes": "Install using uvx package manager. Requires Tavily API key.",
|
||||
"installation_notes": "Install using npx package manager. Requires Tavily API key.",
|
||||
"is_builtin": false,
|
||||
"endorsed": true,
|
||||
"environmentVariables": [
|
||||
@@ -683,7 +683,7 @@
|
||||
"description": "YouTube video transcript extraction",
|
||||
"command": "uvx --from git+https://github.com/jkawamoto/mcp-youtube-transcript mcp-youtube-transcript",
|
||||
"link": "https://github.com/jkawamoto/mcp-youtube-transcript",
|
||||
"installation_notes": "Install using uv package manager.",
|
||||
"installation_notes": "Install using uvx package manager.",
|
||||
"is_builtin": false,
|
||||
"endorsed": false,
|
||||
"environmentVariables": []
|
||||
|
||||
@@ -1615,6 +1615,50 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/import": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Session Management"
|
||||
],
|
||||
"operationId": "import_session",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ImportSessionRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session imported successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Session"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid JSON"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - Invalid or missing API key"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/insights": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1778,6 +1822,51 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/export": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Session Management"
|
||||
],
|
||||
"operationId": "export_session",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"description": "Unique identifier for the session",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session exported successfully",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - Invalid or missing API key"
|
||||
},
|
||||
"404": {
|
||||
"description": "Session not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/user_recipe_values": {
|
||||
"put": {
|
||||
"tags": [
|
||||
@@ -2819,6 +2908,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ImportSessionRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"json"
|
||||
],
|
||||
"properties": {
|
||||
"json": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"InspectJobResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Options as ClientOptions, TDataShape, Client } from './client';
|
||||
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ParseRecipeData, ParseRecipeResponses, ParseRecipeErrors, SaveRecipeData, SaveRecipeResponses, SaveRecipeErrors, ScanRecipeData, ScanRecipeResponses, ReplyData, ReplyResponses, ReplyErrors, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionInsightsData, GetSessionInsightsResponses, GetSessionInsightsErrors, DeleteSessionData, DeleteSessionResponses, DeleteSessionErrors, GetSessionData, GetSessionResponses, GetSessionErrors, UpdateSessionDescriptionData, UpdateSessionDescriptionResponses, UpdateSessionDescriptionErrors, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesResponses, UpdateSessionUserRecipeValuesErrors, StatusData, StatusResponses } from './types.gen';
|
||||
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ParseRecipeData, ParseRecipeResponses, ParseRecipeErrors, SaveRecipeData, SaveRecipeResponses, SaveRecipeErrors, ScanRecipeData, ScanRecipeResponses, ReplyData, ReplyResponses, ReplyErrors, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, ImportSessionData, ImportSessionResponses, ImportSessionErrors, GetSessionInsightsData, GetSessionInsightsResponses, GetSessionInsightsErrors, DeleteSessionData, DeleteSessionResponses, DeleteSessionErrors, GetSessionData, GetSessionResponses, GetSessionErrors, UpdateSessionDescriptionData, UpdateSessionDescriptionResponses, UpdateSessionDescriptionErrors, ExportSessionData, ExportSessionResponses, ExportSessionErrors, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesResponses, UpdateSessionUserRecipeValuesErrors, StatusData, StatusResponses } from './types.gen';
|
||||
import { client as _heyApiClient } from './client.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||
@@ -457,6 +457,17 @@ export const listSessions = <ThrowOnError extends boolean = false>(options?: Opt
|
||||
});
|
||||
};
|
||||
|
||||
export const importSession = <ThrowOnError extends boolean = false>(options: Options<ImportSessionData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<ImportSessionResponses, ImportSessionErrors, ThrowOnError>({
|
||||
url: '/sessions/import',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getSessionInsights = <ThrowOnError extends boolean = false>(options?: Options<GetSessionInsightsData, ThrowOnError>) => {
|
||||
return (options?.client ?? _heyApiClient).get<GetSessionInsightsResponses, GetSessionInsightsErrors, ThrowOnError>({
|
||||
url: '/sessions/insights',
|
||||
@@ -489,6 +500,13 @@ export const updateSessionDescription = <ThrowOnError extends boolean = false>(o
|
||||
});
|
||||
};
|
||||
|
||||
export const exportSession = <ThrowOnError extends boolean = false>(options: Options<ExportSessionData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).get<ExportSessionResponses, ExportSessionErrors, ThrowOnError>({
|
||||
url: '/sessions/{session_id}/export',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSessionUserRecipeValues = <ThrowOnError extends boolean = false>(options: Options<UpdateSessionUserRecipeValuesData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).put<UpdateSessionUserRecipeValuesResponses, UpdateSessionUserRecipeValuesErrors, ThrowOnError>({
|
||||
url: '/sessions/{session_id}/user_recipe_values',
|
||||
|
||||
@@ -341,6 +341,10 @@ export type ImageContent = {
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
export type ImportSessionRequest = {
|
||||
json: string;
|
||||
};
|
||||
|
||||
export type InspectJobResponse = {
|
||||
processStartTime?: string | null;
|
||||
runningDurationSeconds?: number | null;
|
||||
@@ -2177,6 +2181,37 @@ export type ListSessionsResponses = {
|
||||
|
||||
export type ListSessionsResponse = ListSessionsResponses[keyof ListSessionsResponses];
|
||||
|
||||
export type ImportSessionData = {
|
||||
body: ImportSessionRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/sessions/import';
|
||||
};
|
||||
|
||||
export type ImportSessionErrors = {
|
||||
/**
|
||||
* Bad request - Invalid JSON
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Unauthorized - Invalid or missing API key
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type ImportSessionResponses = {
|
||||
/**
|
||||
* Session imported successfully
|
||||
*/
|
||||
200: Session;
|
||||
};
|
||||
|
||||
export type ImportSessionResponse = ImportSessionResponses[keyof ImportSessionResponses];
|
||||
|
||||
export type GetSessionInsightsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -2312,6 +2347,42 @@ export type UpdateSessionDescriptionResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type ExportSessionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Unique identifier for the session
|
||||
*/
|
||||
session_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/sessions/{session_id}/export';
|
||||
};
|
||||
|
||||
export type ExportSessionErrors = {
|
||||
/**
|
||||
* Unauthorized - Invalid or missing API key
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Session not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type ExportSessionResponses = {
|
||||
/**
|
||||
* Session exported successfully
|
||||
*/
|
||||
200: string;
|
||||
};
|
||||
|
||||
export type ExportSessionResponse = ExportSessionResponses[keyof ExportSessionResponses];
|
||||
|
||||
export type UpdateSessionUserRecipeValuesData = {
|
||||
body: UpdateSessionUserRecipeValuesRequest;
|
||||
path: {
|
||||
|
||||
@@ -1183,13 +1183,7 @@ export default function ChatInput({
|
||||
isExtensionsLoading;
|
||||
|
||||
const isUserInputDisabled =
|
||||
isAnyImageLoading ||
|
||||
isAnyDroppedFileLoading ||
|
||||
isRecording ||
|
||||
isTranscribing ||
|
||||
isCompacting ||
|
||||
!agentIsReady ||
|
||||
isExtensionsLoading;
|
||||
isAnyImageLoading || isAnyDroppedFileLoading || isRecording || isTranscribing || isCompacting;
|
||||
|
||||
// Queue management functions - no storage persistence, only in-memory
|
||||
const handleRemoveQueuedMessage = (messageId: string) => {
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Folder,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Download,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
@@ -19,7 +21,14 @@ import { groupSessionsByDate, type DateGroup } from '../../utils/dateUtils';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ConfirmationModal } from '../ui/ConfirmationModal';
|
||||
import { deleteSession, listSessions, Session, updateSessionDescription } from '../../api';
|
||||
import {
|
||||
deleteSession,
|
||||
exportSession,
|
||||
importSession,
|
||||
listSessions,
|
||||
Session,
|
||||
updateSessionDescription,
|
||||
} from '../../api';
|
||||
|
||||
interface EditSessionModalProps {
|
||||
session: Session | null;
|
||||
@@ -210,6 +219,8 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
}
|
||||
};
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const visibleDateGroups = useMemo(() => {
|
||||
return dateGroups.slice(0, visibleGroupsCount);
|
||||
}, [dateGroups, visibleGroupsCount]);
|
||||
@@ -427,14 +438,66 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
setSessionToDelete(null);
|
||||
}, []);
|
||||
|
||||
const handleExportSession = useCallback(async (session: Session, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const response = await exportSession({
|
||||
path: { session_id: session.id },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
const json = response.data;
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${session.description || session.id}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('Session exported successfully');
|
||||
}, []);
|
||||
|
||||
const handleImportClick = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
|
||||
const handleImportSession = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const json = await file.text();
|
||||
await importSession({
|
||||
body: { json },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
toast.success('Session imported successfully');
|
||||
await loadSessions();
|
||||
} catch (error) {
|
||||
toast.error(`Failed to import session: ${error}`);
|
||||
} finally {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
[loadSessions]
|
||||
);
|
||||
|
||||
const SessionItem = React.memo(function SessionItem({
|
||||
session,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
onExportClick,
|
||||
}: {
|
||||
session: Session;
|
||||
onEditClick: (session: Session) => void;
|
||||
onDeleteClick: (session: Session) => void;
|
||||
onExportClick: (session: Session, e: React.MouseEvent) => void;
|
||||
}) {
|
||||
const handleEditClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -456,6 +519,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
onSelectSession(session.id);
|
||||
}, [session.id]);
|
||||
|
||||
const handleExportClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
onExportClick(session, e);
|
||||
},
|
||||
[onExportClick, session]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={handleCardClick}
|
||||
@@ -477,6 +547,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-500 hover:text-red-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportClick}
|
||||
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||
title="Export session"
|
||||
>
|
||||
<Download className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
@@ -598,6 +675,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
session={session}
|
||||
onEditClick={handleEditSession}
|
||||
onDeleteClick={handleDeleteSession}
|
||||
onExportClick={handleExportSession}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -624,6 +702,15 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
<div className="flex flex-col page-transition">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h1 className="text-4xl font-light">Chat history</h1>
|
||||
<Button
|
||||
onClick={handleImportClick}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
Import Session
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mb-4">
|
||||
View and search your past conversations with Goose.
|
||||
@@ -701,6 +788,14 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
</div>
|
||||
</MainPanelLayout>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleImportSession}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<EditSessionModal
|
||||
session={editingSession}
|
||||
isOpen={showEditModal}
|
||||
|
||||
Reference in New Issue
Block a user