Skip to content
Merged
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
44 changes: 44 additions & 0 deletions .agents/skills/pull-request/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: pull-request
description: 自动对比当前分支相对于 master 的差异,推送代码,并使用 `gh` 命令以英文编写标题和描述创建 Pull Request。在用户请求提交 PR、总结更改或推送分支到主库时使用。
---

# Pull Request Creation Workflow

This skill automates the process of creating a Pull Request from the current branch to `master` (or the default base branch) using the GitHub CLI (`gh`).

## Core Objectives

1. **Analyze Diffs**: Compare the current branch with the target base branch (`master` by default) to understand what has changed.
2. **Generate Content**: Write a high-quality PR title and description in **English**, following standard engineering practices (Overview, Key Changes, Verification).
3. **Ensure Sync**: Always push the local branch to the remote origin before creating the PR.
4. **Create PR**: Use `gh pr create` to finalize the process.

## Step-by-Step Instructions

### 1. Research & Analysis
- Determine the current branch name: `git branch --show-current`.
- Verify the status and recent history: `git status && git log -n 5`.
- Analyze the functional changes against the base branch: `git diff master..HEAD --stat`. If the base branch is different, ask the user or check repo defaults.

### 2. Strategy & Preparation
- If there are uncommitted changes, ask the user to commit or stash them first.
- Ensure the branch is pushed: `git push origin <current-branch>`.

### 3. Generate PR Content (English Only)
Draft the title and body in English:
- **Title**: Use conventional commit style, e.g., `feat(ui): add new dashboard`, `fix(auth): resolve session timeout`.
- **Body**:
- `## Overview`: A brief summary of the purpose.
- `## Key Changes`: Bullet points describing specific implementation details.
- `## Verification`: How the changes were tested.

### 4. Execution
- Call `gh pr create` with the generated title and body.
- If `gh` is not in the `PATH`, look for it in common locations like `/opt/homebrew/bin/gh` or `/usr/local/bin/gh`.
- Default flags: `--base master --head <current-branch> --web=false`.

## Example Command
```bash
gh pr create --title "feat(observability): implement tracing" --body "## Overview..." --base master --head feat/branch-name
```
14 changes: 13 additions & 1 deletion backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"

"vstable-engine/internal/ast"
Expand Down Expand Up @@ -125,13 +126,24 @@ func UnaryInterceptor(
}
}()

traceID := "unknown"
if md, ok := metadata.FromIncomingContext(ctx); ok {
if vals := md.Get("x-trace-id"); len(vals) > 0 {
traceID = vals[0]
}
}

log.Printf("[%s] gRPC Request: %s | payload: %+v", traceID, info.FullMethod, req)

resp, err = handler(ctx, req)
if err != nil {
// Just ensure it's a grpc status error
if _, ok := status.FromError(err); !ok {
err = status.Errorf(codes.Unknown, "%v", err)
}
log.Printf("[gRPC Error] %s: %v", info.FullMethod, err)
log.Printf("[%s] gRPC Response: %s | error: %v", traceID, info.FullMethod, err)
} else {
log.Printf("[%s] gRPC Response: %s | success", traceID, info.FullMethod)
}
return resp, err
}
Expand Down
14 changes: 12 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@tailwindcss/postcss": "^4.1.18",
"@tauri-apps/plugin-log": "^2.8.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-store": "^2.2.0",
"lucide-react": "^0.563.0",
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core';
import { LazyStore } from '@tauri-apps/plugin-store';
import { info } from '@tauri-apps/plugin-log';
import type { ConnectionConfig, PersistedWorkspace, QueryResult } from '../types/session';

/**
Expand All @@ -9,6 +10,14 @@ import type { ConnectionConfig, PersistedWorkspace, QueryResult } from '../types

const store = new LazyStore('settings.json');

const callApi = async <T>(command: string, args: Record<string, any> = {}): Promise<T> => {
const traceId = crypto.randomUUID();
const payload = { ...args, traceId };
// Log the outgoing request to the unified log file
info(`[${traceId}] IPC Call: ${command} | args: ${JSON.stringify(args)}`);
return invoke<T>(command, payload);
};

export const apiClient = {
// Database Operations
connect: async (id: string, config: ConnectionConfig): Promise<QueryResult> => {
Expand All @@ -19,23 +28,23 @@ export const apiClient = {
} else {
dsn = `postgres://${user}:${password}@${host}:${port}/${database}?sslmode=disable`;
}
return invoke('db_connect', { id, dialect, dsn });
return callApi('db_connect', { id, dialect, dsn });
},

query: async (id: string, sql: string, params?: any[]): Promise<QueryResult> =>
invoke('db_query', { id, sql, params }),
callApi('db_query', { id, sql, params }),

disconnect: async (id: string): Promise<void> => invoke('db_disconnect', { id }),
disconnect: async (id: string): Promise<void> => callApi('db_disconnect', { id }),

enginePing: async (): Promise<boolean> => invoke('engine_ping'),
enginePing: async (): Promise<boolean> => callApi('engine_ping'),

generateAlterSql: async (req: any): Promise<string[]> => {
const res: any = await invoke('sql_generate_alter', { req });
const res: any = await callApi('sql_generate_alter', { req });
return res.sqls || [];
},

generateCreateSql: async (req: any): Promise<string[]> => {
const res: any = await invoke('sql_generate_create', { req });
const res: any = await callApi('sql_generate_create', { req });
return res.sqls || [];
},

Expand Down
38 changes: 28 additions & 10 deletions frontend/tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ use crate::vstable::{ConnectRequest, DisconnectRequest, QueryRequest, PingReques
use crate::vstable::engine_service_client::EngineServiceClient;
use crate::utils::{json_to_prost_value, prost_struct_to_json, json_to_diff_request};

fn inject_trace_id<T>(mut req: tonic::Request<T>, trace_id: &str) -> tonic::Request<T> {
if let Ok(meta_value) = trace_id.parse() {
req.metadata_mut().insert("x-trace-id", meta_value);
}
req
}

#[tauri::command]
pub async fn db_connect(
state: State<'_, GrpcState>,
trace_id: String,
id: String,
dialect: String,
dsn: String,
) -> Result<serde_json::Value, String> {
log::info!("[{}] Received IPC: db_connect | id: {}, dialect: {}", trace_id, id, dialect);
let mut client_lock = state.client.lock().await;
let addr = format!("http://127.0.0.1:{}", state.port);
let channel = tonic::transport::Endpoint::from_shared(addr)
Expand All @@ -20,7 +29,7 @@ pub async fn db_connect(
.map_err(|e| e.to_string())?;

let mut client = EngineServiceClient::new(channel);
let request = tonic::Request::new(ConnectRequest { id, dialect, dsn });
let request = inject_trace_id(tonic::Request::new(ConnectRequest { id, dialect, dsn }), &trace_id);
let response = client.db_connect(request).await.map_err(|e| e.to_string())?;
let inner = response.into_inner();

Expand All @@ -31,18 +40,20 @@ pub async fn db_connect(
#[tauri::command]
pub async fn db_query(
state: State<'_, GrpcState>,
trace_id: String,
id: String,
sql: String,
params: Option<Vec<serde_json::Value>>,
) -> Result<serde_json::Value, String> {
log::info!("[{}] Received IPC: db_query | id: {}, sql length: {}", trace_id, id, sql.len());
let mut client_lock = state.client.lock().await;
let client = client_lock.as_mut().ok_or("Not connected")?;

let pb_params = params.map(|p| prost_types::ListValue {
values: p.into_iter().map(json_to_prost_value).collect(),
});

let request = tonic::Request::new(QueryRequest { id, sql, params: pb_params });
let request = inject_trace_id(tonic::Request::new(QueryRequest { id, sql, params: pb_params }), &trace_id);
let response = client.query(request).await.map_err(|e| e.to_string())?;
let inner = response.into_inner();

Expand All @@ -54,16 +65,18 @@ pub async fn db_query(
}

#[tauri::command]
pub async fn db_disconnect(state: State<'_, GrpcState>, id: String) -> Result<(), String> {
pub async fn db_disconnect(state: State<'_, GrpcState>, trace_id: String, id: String) -> Result<(), String> {
log::info!("[{}] Received IPC: db_disconnect | id: {}", trace_id, id);
let mut client_lock = state.client.lock().await;
let client = client_lock.as_mut().ok_or("Not connected")?;
let request = tonic::Request::new(DisconnectRequest { id });
let request = inject_trace_id(tonic::Request::new(DisconnectRequest { id }), &trace_id);
client.disconnect(request).await.map_err(|e| e.to_string())?;
Ok(())
}

#[tauri::command]
pub async fn engine_ping(state: State<'_, GrpcState>) -> Result<bool, String> {
pub async fn engine_ping(state: State<'_, GrpcState>, trace_id: String) -> Result<bool, String> {
log::info!("[{}] Received IPC: engine_ping", trace_id);
let mut client_lock = state.client.lock().await;
let addr = format!("http://127.0.0.1:{}", state.port);
let channel = tonic::transport::Endpoint::from_shared(addr)
Expand All @@ -73,26 +86,31 @@ pub async fn engine_ping(state: State<'_, GrpcState>) -> Result<bool, String> {
.map_err(|e| e.to_string())?;

let mut client = EngineServiceClient::new(channel);
let result = client.ping(tonic::Request::new(PingRequest {})).await.is_ok();
let request = inject_trace_id(tonic::Request::new(PingRequest {}), &trace_id);
let result = client.ping(request).await.is_ok();
*client_lock = Some(client);
Ok(result)
}

#[tauri::command]
pub async fn sql_generate_alter(state: State<'_, GrpcState>, req: serde_json::Value) -> Result<serde_json::Value, String> {
pub async fn sql_generate_alter(state: State<'_, GrpcState>, trace_id: String, req: serde_json::Value) -> Result<serde_json::Value, String> {
log::info!("[{}] Received IPC: sql_generate_alter | req: {}", trace_id, req);
let mut client_lock = state.client.lock().await;
let client = client_lock.as_mut().ok_or("Not connected")?;
let request = tonic::Request::new(json_to_diff_request(req));
let diff_req = json_to_diff_request(req)?;
let request = inject_trace_id(tonic::Request::new(diff_req), &trace_id);
let response = client.generate_alter_table(request).await.map_err(|e| e.to_string())?;
let inner = response.into_inner();
Ok(serde_json::json!({ "success": inner.success, "sqls": inner.sqls }))
}

#[tauri::command]
pub async fn sql_generate_create(state: State<'_, GrpcState>, req: serde_json::Value) -> Result<serde_json::Value, String> {
pub async fn sql_generate_create(state: State<'_, GrpcState>, trace_id: String, req: serde_json::Value) -> Result<serde_json::Value, String> {
log::info!("[{}] Received IPC: sql_generate_create | req: {}", trace_id, req);
let mut client_lock = state.client.lock().await;
let client = client_lock.as_mut().ok_or("Not connected")?;
let request = tonic::Request::new(json_to_diff_request(req));
let diff_req = json_to_diff_request(req)?;
let request = inject_trace_id(tonic::Request::new(diff_req), &trace_id);
let response = client.generate_create_table(request).await.map_err(|e| e.to_string())?;
let inner = response.into_inner();
Ok(serde_json::json!({ "success": inner.success, "sqls": inner.sqls }))
Expand Down
21 changes: 19 additions & 2 deletions frontend/tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,27 @@ use tauri_plugin_shell::ShellExt;
use std::sync::Arc;
use tokio::sync::Mutex;
use grpc::GrpcState;
use tauri_plugin_log::{Target, TargetKind, RotationStrategy};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let log_dir = std::env::current_exe().unwrap().parent().unwrap().join("logs");

tauri::Builder::default()
.plugin(
tauri_plugin_log::Builder::new()
.targets([
Target::new(TargetKind::Stdout),
Target::new(TargetKind::Folder {
path: log_dir,
file_name: Some("vstable".to_string()),
}),
Target::new(TargetKind::Webview),
])
.rotation_strategy(RotationStrategy::KeepAll)
.max_file_size(50 * 1024 * 1024)
.build()
)
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::default().build())
.manage(GrpcState { client: Arc::new(Mutex::new(None)), port: 39082 })
Expand All @@ -34,9 +51,9 @@ pub fn run() {
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
println!("Sidecar: {}", String::from_utf8_lossy(&line));
log::info!("Sidecar: {}", String::from_utf8_lossy(&line));
} else if let tauri_plugin_shell::process::CommandEvent::Stderr(line) = event {
eprintln!("Sidecar Error: {}", String::from_utf8_lossy(&line));
log::error!("Sidecar Error: {}", String::from_utf8_lossy(&line));
}
}
});
Expand Down
Loading
Loading