Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,417 changes: 1,416 additions & 1 deletion apps/chat2db-web/src/legacy.rs

Large diffs are not rendered by default.

127 changes: 126 additions & 1 deletion apps/chat2db-web/src/legacy_ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ pub struct LegacyAiSessionDeleteRequest {
pub id: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LegacyAiSessionRenameRequest {
pub id: String,
pub title: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LegacyAiMessagesQuery {
Expand Down Expand Up @@ -662,6 +669,38 @@ pub async fn delete_session(application: &Application, id: &str) -> Result<(), L
Ok(())
}

/// Renames one durable Community AI session through revision CAS.
///
/// # Errors
///
/// Returns validation, lookup, revision, or storage failures.
pub async fn rename_session(
application: &Application,
id: &str,
title: &str,
) -> Result<(), LegacyAiFailure> {
let title = title.trim();
if title.is_empty() {
return Err(LegacyAiFailure::invalid(
"invalid_ai_request",
"title must not be empty",
));
}
let session = application.get_agent_session(id.trim()).await?;
application
.update_agent_session(
&session.id,
UpdateAgentSessionRequest {
expected_revision: session.revision.clone(),
title: title.to_owned(),
provider_id: session.provider_id.clone(),
datasource_id: session.datasource_id.clone(),
},
)
.await?;
Ok(())
}

/// Lists provider profiles that have usable credentials as frontend model options.
///
/// # Errors
Expand Down Expand Up @@ -1088,6 +1127,23 @@ pub async fn dispatch(
.unwrap_or_else(|_| internal_failure_value()),
)
}
("post", "/api/v3/ai/chat/history/session/rename") => {
let request = serde_json::from_value::<LegacyAiSessionRenameRequest>(message);
let result = match request {
Ok(request) => rename_session(application, &request.id, &request.title).await,
Err(_) => Err(LegacyAiFailure::invalid(
"invalid_ai_request",
"id and title must not be empty",
)),
};
Some(
serde_json::to_value(match result {
Ok(()) => LegacyAiEnvelope::success(()),
Err(error) => LegacyAiEnvelope::failure(error),
})
.unwrap_or_else(|_| internal_failure_value()),
)
}
("get", "/api/v3/ai/model/options") => Some(
serde_json::to_value(match model_options(application).await {
Ok(data) => LegacyAiEnvelope::success(data),
Expand Down Expand Up @@ -1186,6 +1242,10 @@ pub(crate) fn routes() -> Router<Application> {
"/api/v3/ai/chat/history/session/delete",
post(delete_session_handler),
)
.route(
"/api/v3/ai/chat/history/session/rename",
post(rename_session_handler),
)
.route("/api/v3/ai/model/options", get(model_options_handler))
.route("/api/v3/ai/model/list", get(model_catalog_handler))
.route(
Expand Down Expand Up @@ -1312,6 +1372,21 @@ async fn delete_session_handler(
)
}

async fn rename_session_handler(
State(application): State<Application>,
Json(request): Json<LegacyAiSessionRenameRequest>,
) -> Json<serde_json::Value> {
Json(
serde_json::to_value(
match rename_session(&application, &request.id, &request.title).await {
Ok(()) => LegacyAiEnvelope::success(()),
Err(error) => LegacyAiEnvelope::failure(error),
},
)
.unwrap_or_else(|_| internal_failure_value()),
)
}

async fn model_options_handler(State(application): State<Application>) -> Json<serde_json::Value> {
Json(
serde_json::to_value(match model_options(&application).await {
Expand Down Expand Up @@ -1978,7 +2053,7 @@ mod tests {
};
use chat2db_contract::{
AgentEvent, AgentEventEnvelope, AgentPermissionRequest, ApiError,
CreateProviderProfileRequest, ProviderKind,
CreateAgentSessionRequest, CreateProviderProfileRequest, ProviderKind,
};
use chat2db_core::Application;
use chat2db_storage::{SecretRef, SecretValue, SecretVault, SecretVaultError, Storage};
Expand Down Expand Up @@ -2454,4 +2529,54 @@ mod tests {
.contains("alpha")
);
}

#[tokio::test]
async fn session_rename_route_updates_the_visible_title() {
let fixture = test_application();
let profile = fixture
.application
.create_provider_profile(CreateProviderProfileRequest {
name: "Rename provider".to_owned(),
kind: ProviderKind::OpenAiCompatible,
base_url: "https://provider.example/v1".to_owned(),
model: "mock-model".to_owned(),
context_window_tokens: "4096".to_owned(),
max_output_tokens: "1024".to_owned(),
credentials: None,
})
.await
.expect("provider profile must be created");
let session = fixture
.application
.create_agent_session(CreateAgentSessionRequest {
title: "First title".to_owned(),
provider_id: profile.id,
datasource_id: None,
system_prompt: None,
})
.await
.expect("session must be created");
let application = routes().with_state(fixture.application);

let renamed = application
.clone()
.oneshot(json_request(
Method::POST,
"/api/v3/ai/chat/history/session/rename",
&serde_json::json!({ "id": session.id, "title": "Renamed title" }),
))
.await
.expect("rename route must respond");
assert_eq!(response_json(renamed).await["success"], true);

let sessions = application
.oneshot(empty_request(
Method::GET,
"/api/v3/ai/chat/history/sessions",
))
.await
.expect("session list must respond");
let sessions = response_json(sessions).await;
assert_eq!(sessions["data"][0]["title"], "Renamed title");
}
}
2 changes: 2 additions & 0 deletions apps/frontend/src/generated/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,8 @@ export interface components {
hasSecret: boolean;
/** @description Opaque datasource id. */
id: string;
/** @description Optional `#RRGGBB` identity colour shown by the connection tree. */
identityColor?: string | null;
/** @description User-visible datasource name. */
name: string;
/** @description Monotonic revision encoded as a decimal integer. */
Expand Down
7 changes: 7 additions & 0 deletions contracts/openapi/chat2db-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -7211,6 +7211,13 @@
"type": "string",
"description": "Opaque datasource id."
},
"identityColor": {
"type": [
"string",
"null"
],
"description": "Optional `#RRGGBB` identity colour shown by the connection tree."
},
"name": {
"type": "string",
"description": "User-visible datasource name."
Expand Down
35 changes: 35 additions & 0 deletions crates/chat2db-contract/src/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2049,3 +2049,38 @@ mod tests {
}
}
}

/// One active `InnoDB` transaction reported by the `MySQL` operations monitor.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ActiveTransaction {
/// `InnoDB` transaction id.
pub trx_id: String,
/// `InnoDB` transaction state.
pub state: String,
/// Transaction start time as Unix epoch milliseconds.
pub started_at_ms: String,
/// Whole seconds since the transaction started.
pub age_seconds: u64,
/// Isolation level reported by `InnoDB`.
pub isolation_level: String,
/// Rows currently locked by the transaction.
pub rows_locked: u64,
/// Rows modified by the transaction.
pub rows_modified: u64,
/// `InnoDB` lock structures held by the transaction.
pub lock_structs: u64,
/// `MySQL` thread id owning the transaction.
pub thread_id: u64,
/// Connection user, when the process list entry is visible.
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
/// Connection host, when the process list entry is visible.
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
/// Default database of the connection, when visible.
#[serde(skip_serializing_if = "Option::is_none")]
pub database: Option<String>,
/// Currently running statement, empty when the session is idle.
pub query: String,
}
4 changes: 4 additions & 0 deletions crates/chat2db-contract/src/datasource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ pub struct Datasource {
pub created_at_ms: String,
/// Unix epoch milliseconds encoded as a decimal integer.
pub updated_at_ms: String,
/// Optional `#RRGGBB` identity colour shown by the connection tree.
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_color: Option<String>,
}

/// Stable datasource collection returned by list APIs.
Expand Down Expand Up @@ -142,6 +145,7 @@ mod tests {
revision: "9007199254740993".to_owned(),
created_at_ms: "1784900000000".to_owned(),
updated_at_ms: "1784900000001".to_owned(),
identity_color: None,
};

let json = serde_json::to_string(&response).expect("response must serialize");
Expand Down
5 changes: 5 additions & 0 deletions crates/chat2db-contract/src/datasource_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pub struct DatasourceEditProjection {
pub has_secret: bool,
/// Monotonic revision encoded as a decimal integer.
pub revision: String,
/// Optional `#RRGGBB` identity colour shown by the connection tree.
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_color: Option<String>,
}

impl Debug for DatasourceEditProjection {
Expand All @@ -47,6 +50,7 @@ impl Debug for DatasourceEditProjection {
.field("ssh", &self.ssh)
.field("has_secret", &self.has_secret)
.field("revision", &self.revision)
.field("identity_color", &self.identity_color)
.finish()
}
}
Expand All @@ -72,6 +76,7 @@ mod tests {
ssh: None,
has_secret: true,
revision: "3".to_owned(),
identity_color: None,
};

let json = serde_json::to_string(&projection).expect("projection serializes");
Expand Down
22 changes: 12 additions & 10 deletions crates/chat2db-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ pub use agent::{
UpdateProviderProfileRequest,
};
pub use community::{
BuildCommunityCreateSchemaRequest, BuildCommunityDmlRequest, BuildCommunityNamespaceSqlRequest,
CommunityBuiltSql, CommunityDatabase, CommunityDatabaseList, CommunityDmlAssignment,
CommunityDmlColumn, CommunityDmlRow, CommunityDmlStatement, CommunityDmlTarget,
CommunityDmlTemporalKind, CommunityDmlValue, CommunityDriverConfig, CommunityForeignKey,
CommunityForeignKeyList, CommunityFormattedSql, CommunityFunction, CommunityFunctionList,
CommunityFunctionParameter, CommunityFunctionParameterList, CommunityNamespaceSqlOperation,
CommunityParsedStatement, CommunityPlugin, CommunityPluginBehavior, CommunityPluginCatalog,
CommunityPluginServices, CommunityPrimaryKey, CommunityPrimaryKeyList, CommunityProcedure,
CommunityProcedureList, CommunityProcedureParameter, CommunityProcedureParameterList,
ActiveTransaction, BuildCommunityCreateSchemaRequest, BuildCommunityDmlRequest,
BuildCommunityNamespaceSqlRequest, CommunityBuiltSql, CommunityDatabase, CommunityDatabaseList,
CommunityDmlAssignment, CommunityDmlColumn, CommunityDmlRow, CommunityDmlStatement,
CommunityDmlTarget, CommunityDmlTemporalKind, CommunityDmlValue, CommunityDriverConfig,
CommunityForeignKey, CommunityForeignKeyList, CommunityFormattedSql, CommunityFunction,
CommunityFunctionList, CommunityFunctionParameter, CommunityFunctionParameterList,
CommunityNamespaceSqlOperation, CommunityParsedStatement, CommunityPlugin,
CommunityPluginBehavior, CommunityPluginCatalog, CommunityPluginServices, CommunityPrimaryKey,
CommunityPrimaryKeyList, CommunityProcedure, CommunityProcedureList,
CommunityProcedureParameter, CommunityProcedureParameterList,
CommunityRoutineInvocationPreview, CommunityRoutineMigrationExecution,
CommunityRoutineMigrationRequest, CommunitySchema, CommunitySchemaList, CommunitySqlAnalysis,
CommunitySqlCompletion, CommunitySqlCompletionActiveSnippetSlot,
Expand Down Expand Up @@ -115,7 +116,8 @@ pub use transfer::{
DmlExportFormat, DmlExportRequest, DmlExportSize, GenerateMysqlClassRequest,
GeneratedMysqlClassSet, ImportFileRequest, OtherFileExportRequest, SqlFileExportRequest,
TabularImportEncoding, TransferArtifact, TransferFileFormat, TransferSqlScope, TransferTask,
TransferTaskAccepted, TransferTaskKind, TransferTaskPage, TransferTaskStatus,
TransferTaskAccepted, TransferTaskEvent, TransferTaskKind, TransferTaskPage,
TransferTaskStatus,
};
pub use workspace::{
AssignDatasourceNamespaceRequest, CreateWorkspaceNamespaceRequest, MoveWorkspaceNodeRequest,
Expand Down
23 changes: 23 additions & 0 deletions crates/chat2db-contract/src/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,29 @@ pub struct GeneratedMysqlClassSet {
pub files: Vec<String>,
}

/// One ordered progress event of a durable transfer task.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TransferTaskEvent {
/// Event row id.
pub id: i64,
/// Owning transfer task id.
pub task_id: i64,
/// Monotonic per-task sequence starting at one.
pub sequence: u64,
/// `INFO`, `WARN`, or `ERROR`.
pub level: String,
/// Stable machine-readable event code, empty when none applies.
pub code: String,
/// Optional pipeline stage.
#[serde(skip_serializing_if = "Option::is_none")]
pub stage: Option<String>,
/// Human-readable event message.
pub message: String,
/// Unix epoch milliseconds encoded as a decimal integer.
pub created_at_ms: String,
}

const fn default_true() -> bool {
true
}
Expand Down
1 change: 1 addition & 0 deletions crates/chat2db-core/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ pub(crate) fn datasource(record: storage::DatasourceRecord) -> contract::Datasou
revision: record.revision.to_string(),
created_at_ms: record.created_at_ms.to_string(),
updated_at_ms: record.updated_at_ms.to_string(),
identity_color: record.identity_color,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/chat2db-core/src/datasource_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl Application {
ssh: projected.ssh,
has_secret,
revision: record.revision.to_string(),
identity_color: record.identity_color,
})
}

Expand Down
Loading
Loading