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
124 changes: 124 additions & 0 deletions Cargo.lock

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

16 changes: 16 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ Ask는 Redrob Launcher가 이 컴퓨터를 로그인시킨 계정을 씁니다.
그대로 보여 줍니다. 계정이 없으면 Ask 도크가 그렇다고 말하고 런처를 열어 줍니다. 이 앱에는 자체 로그인이
없습니다.

## Redrob Code에서 조작

앱이 열려 있는 동안 자기 도구를 공유 Redrob Code 데몬에 루프백 MCP(`app:query`)로 제공하므로, 어느 Redrob
표면의 에이전트든(휴대폰 채팅 포함) 이 앱을 움직일 수 있습니다. 데몬은 선택 사항이며 없으면 앱은 그대로
동작합니다. 도구는 짧은 TSV 텍스트로 답하고 패널이 쓰는 엔진 경로를 그대로 지나가므로, 브리지로 들어온
질의도 탭에 입력한 것과 똑같이 한도와 게이트를 받습니다.

- `list_connections` - 저장된 연결 목록: id, 이름, 드라이버, 연결 여부.
- `connect {id}` - 저장된 연결을 id 또는 이름으로 연결하고, 올라오면 답합니다.
- `list_tables {connection?, schema?}` - 연결된 데이터베이스의 표·뷰·컬렉션.
- `describe_table {connection?, table}` - 한 표의 열(이름, 타입, null 허용).
- `run_sql {connection?, sql, max_rows?}` - 읽기 전용 문장 하나를 실행. TSV 헤더와 행(기본 200행), 그다음
개수·시간·더 있는지 여부를 담은 요약 한 줄.
- `open_in_editor {sql, connection?}` - 실행하지 않고 새 편집기 탭에 텍스트를 넣습니다.
- `export_result {path, format}` - 마지막 `run_sql` 결과(없으면 활성 탭의 그리드)를 CSV 또는 TSV로 씁니다.

## 현재 경계

- 자유 형식 쓰기 SQL은 없습니다. 변경은 검토한 편집 집합과 표 폼으로만 갑니다. 고치려면 단일 열 기본 키가
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ environment, then this app's own `account.json`, win over it. The key is sent on
refuses. Without an account the Ask dock says so and offers to open the launcher; this app has no
sign-in of its own.

## Control from Redrob Code

While the app is open it serves its tools to the shared Redrob Code daemon over loopback MCP (as
`app:query`), so the agent in any Redrob surface, a phone chat included, can drive it. The daemon is
optional: without one the app runs as before. The tools answer short TSV text and go through the
same engine paths the panels use, so a bridged query is bounded and gated exactly like one typed
into a tab.

- `list_connections` - the saved connections: id, name, driver, connected.
- `connect {id}` - connect a saved connection by id or name; answers when it is up.
- `list_tables {connection?, schema?}` - tables, views or collections of a connected database.
- `describe_table {connection?, table}` - the columns (name, type, nullable) of one table.
- `run_sql {connection?, sql, max_rows?}` - run one read-only statement; TSV header and rows
(200 by default), then a summary line with the count, the time and whether more rows exist.
- `open_in_editor {sql, connection?}` - put the text in a new editor tab without running it.
- `export_result {path, format}` - write the last `run_sql` result (else the active tab's grid) as
CSV or TSV.

## Current boundaries

- No free-form write SQL: changes go through the reviewed edit set and the table forms. Tables need a
Expand Down
2 changes: 2 additions & 0 deletions apps/redrob-query/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ path = "src/main.rs"

[dependencies]
redrob-query-core = { path = "../../crates/redrob-query-core" }
# The Redrob Code app bridge: serves this app's tools to the shared daemon over loopback MCP.
redrob-app-bridge = { git = "https://github.com/redrob-labs/redrob-code", tag = "v0.7.0" }
eframe = { workspace = true, features = ["wgpu", "accesskit", "wayland", "x11", "persistence", "default_fonts"] }
egui = { workspace = true }
egui_extras = { workspace = true }
Expand Down
25 changes: 16 additions & 9 deletions apps/redrob-query/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use uuid::Uuid;

use crate::account::AccountWatch;
use crate::ai_dock::{AiDock, AiEvent};
use crate::app_bridge::Bridge;
use crate::changes::{ChangesEvent, ChangesPanel};
use crate::editor::{QueryTab, TabEvent};
use crate::highlight::Dialect;
Expand Down Expand Up @@ -72,15 +73,15 @@ const SHORTCUTS: &[(&str, &str)] = &[
];

pub struct QueryApp {
workspace: Workspace,
profiles: ProfilesPanel,
pub(crate) workspace: Workspace,
pub(crate) profiles: ProfilesPanel,
schema: SchemaBrowser,
structure: StructurePanel,
changes: ChangesPanel,
ai: AiDock,
account: AccountWatch,
tabs: Vec<QueryTab>,
active: usize,
pub(crate) tabs: Vec<QueryTab>,
pub(crate) active: usize,
history: Vec<HistoryEntry>,
show_structure: bool,
show_ai: bool,
Expand All @@ -91,8 +92,10 @@ pub struct QueryApp {
status: Option<Status>,
language: i18n::Language,
theme_pref: ThemePref,
tab_counter: usize,
pub(crate) tab_counter: usize,
dirty_since_save: bool,
/// The Redrob Code app bridge (`app_bridge.rs`); the app runs without the daemon too.
pub(crate) bridge: Bridge,
}

impl QueryApp {
Expand All @@ -118,6 +121,7 @@ impl QueryApp {
});
let repaint = ctx.clone();
workspace.set_wake(move || repaint.request_repaint());
let bridge = Bridge::start(ctx);

let mut tabs: Vec<QueryTab> = saved
.tabs
Expand Down Expand Up @@ -152,12 +156,13 @@ impl QueryApp {
theme_pref,
tab_counter,
dirty_since_save: false,
bridge,
workspace,
}
}

/// Show `text` on the status line (an error in the error colour).
fn notify(&mut self, text: impl Into<String>, is_error: bool) {
pub(crate) fn notify(&mut self, text: impl Into<String>, is_error: bool) {
let text = text.into();
if is_error {
log::warn!("{text}");
Expand All @@ -174,7 +179,7 @@ impl QueryApp {
}

/// The connection the shell acts on: the active tab's, else the selected one.
fn active_profile(&self) -> Option<&ConnectionProfile> {
pub(crate) fn active_profile(&self) -> Option<&ConnectionProfile> {
self.tabs
.get(self.active)
.and_then(|tab| self.profile(tab.connection_id))
Expand Down Expand Up @@ -211,7 +216,7 @@ impl QueryApp {
self.dirty_since_save = false;
}

fn push_tab(&mut self, mut tab: QueryTab) {
pub(crate) fn push_tab(&mut self, mut tab: QueryTab) {
if self.tabs.len() >= crate::workspace::MAX_TABS {
self.notify(
tf(
Expand All @@ -228,7 +233,7 @@ impl QueryApp {
self.dirty_since_save = true;
}

fn open_tab_for(&mut self, profile: &ConnectionProfile, text: Option<String>) {
pub(crate) fn open_tab_for(&mut self, profile: &ConnectionProfile, text: Option<String>) {
// Re-use an open tab on this connection that is still its untouched starter.
if text.is_none()
&& let Some(index) = self.tabs.iter().position(|t| {
Expand Down Expand Up @@ -1275,6 +1280,7 @@ impl QueryApp {
impl eframe::App for QueryApp {
fn ui(&mut self, ui: &mut Ui, _frame: &mut eframe::Frame) {
self.poll();
self.serve_bridge();
self.handle_shortcuts(ui.ctx());

self.top_bar(ui);
Expand Down Expand Up @@ -1356,6 +1362,7 @@ impl eframe::App for QueryApp {
}

fn on_exit(&mut self) {
self.bridge.stop();
self.save_workspace();
}
}
Loading
Loading