You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/23 13:07:10 UTC
(maka) branch feat/runtime-host-rust updated: feat(tui): preserve complete pending message inputs
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
new 0bc19fb2d8 feat(tui): preserve complete pending message inputs
0bc19fb2d8 is described below
commit 0bc19fb2d8606ffda4879cda11bf7c10d0768945
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Wed Sep 23 21:06:44 2026 +0800
feat(tui): preserve complete pending message inputs
Persist native content, references and input intent through checkpoint and explicit retry. Migrate text-only checkpoints without changing request identity, and reject mismatched completion payloads.
Generated-by: Codex
---
crates/cli/tests/integration/tui/reading.rs | 2 +-
crates/cli/tests/integration/tui/recovery.rs | 25 ++++-
crates/tui/locales/en.ftl | 2 +-
crates/tui/locales/zh-CN.ftl | 2 +-
crates/tui/locales/zh-TW.ftl | 2 +-
crates/tui/src/pages/sending.rs | 43 ++++-----
crates/tui/src/state.rs | 1 +
crates/tui/src/state/snapshot.rs | 136 +++++++++++++++++++++++++--
crates/tui/src/state/submission.rs | 68 ++++++++++++++
9 files changed, 243 insertions(+), 38 deletions(-)
diff --git a/crates/cli/tests/integration/tui/reading.rs b/crates/cli/tests/integration/tui/reading.rs
index 8516afc8ed..1ab4ad3ee7 100644
--- a/crates/cli/tests/integration/tui/reading.rs
+++ b/crates/cli/tests/integration/tui/reading.rs
@@ -151,7 +151,7 @@ fn old_window_selection_and_search_resume_across_pages_and_disk_restart() {
.unwrap(),
)
.unwrap();
- assert_eq!(saved["version"], 6);
+ assert_eq!(saved["version"], 7);
let encoded = serde_json::to_string(&saved).unwrap();
assert!(
!encoded.contains("History context line"),
diff --git a/crates/cli/tests/integration/tui/recovery.rs b/crates/cli/tests/integration/tui/recovery.rs
index 7ae7f3a12d..2cc5a22c82 100644
--- a/crates/cli/tests/integration/tui/recovery.rs
+++ b/crates/cli/tests/integration/tui/recovery.rs
@@ -76,9 +76,9 @@ fn recover(after_acceptance: bool) {
tui.wait_for("storage offline edit");
std::fs::remove_dir(&checkpoint).unwrap();
tui.send(b"\x10");
- tui.wait_for("Retry original message (same ID and text)");
+ tui.wait_for("Retry original message");
assert!(proxy.requests.lock().unwrap().is_empty(), "storage recovery must not automatically resend");
- tui.click_text("Retry original message (same ID and text)");
+ tui.click_text("Retry original message");
}
tui.wait_for("Delivery uncertain");
tui.wait_for("connection failed"); // The request failure can precede the connection-close observation.
@@ -99,6 +99,18 @@ fn recover(after_acceptance: bool) {
// SIGKILL cannot run the normal quit flush; only the prior checkpoint survives.
tui.child.kill().unwrap();
assert!(!tui.child.wait().unwrap().success());
+ // Reopen an actual v6 text-only checkpoint, not a synthetic Host
+ // response. The migration must retain the same unknown identity.
+ let mut legacy: serde_json::Value = serde_json::from_slice(&std::fs::read(&checkpoint).unwrap()).unwrap();
+ legacy["version"] = json!(6);
+ for request in legacy["unresolved"].as_array_mut().unwrap() {
+ let fields = request.as_object_mut().unwrap();
+ let content = fields.remove("content").unwrap();
+ fields.insert("text".into(), content["text"].clone());
+ fields.remove("input_selections");
+ fields.remove("turn_orchestration");
+ }
+ std::fs::write(&checkpoint, serde_json::to_vec(&legacy).unwrap()).unwrap();
}
let mut tui = Pty::spawn(&["--root", host.root.to_str().unwrap()]);
tui.wait_for(if after_acceptance { "Delivered once." } else { "No messages yet." });
@@ -106,10 +118,10 @@ fn recover(after_acceptance: bool) {
tui.wait_for("Delivery uncertain");
assert_eq!(proxy.requests.lock().unwrap().len(), 1, "reopening must not automatically replay");
tui.send(b"\x10");
- tui.wait_for("Retry original message (same ID and text)");
+ tui.wait_for("Retry original message");
// wait_for already waits for a complete synchronized frame. An unrelated
// last command may be below the viewport when a live turn adds actions.
- tui.click_text("Retry original message (same ID and text)");
+ tui.click_text("Retry original message");
// Closing the palette can temporarily cover/erase the old feedback before
// the checkpoint releases the explicit retry. Observe the real relay too.
tui.wait_until(|screen| proxy.requests.lock().unwrap().len() == 2 && !screen.contains("Delivery uncertain") && !screen.contains("Sending…") && !screen.contains("Commands · Esc") && screen.contains("plus new edits"));
@@ -248,8 +260,11 @@ impl LostReply {
assert_eq!(original["origin_epoch"], value["input"]["originHostEpoch"]);
assert_eq!(original["session"], value["input"]["sessionId"]);
assert_eq!(original["id"], value["input"]["messageId"]);
- assert_eq!(original["text"], value["input"]["content"]["text"]);
+ assert_eq!(saved["version"], 7);
+ assert_eq!(original["content"], value["input"]["content"]);
assert_eq!(original["placement"], value["input"]["placement"]);
+ assert_eq!(original["input_selections"], value["input"].get("inputSelections").cloned().unwrap_or_else(|| json!({})));
+ assert_eq!(original["turn_orchestration"], value["input"]["turnOrchestration"]);
captured.lock().unwrap().push(value["input"].clone());
if lose && !after_acceptance {
lose = false;
diff --git a/crates/tui/locales/en.ftl b/crates/tui/locales/en.ftl
index 874102a104..78d8c8ab5b 100644
--- a/crates/tui/locales/en.ftl
+++ b/crates/tui/locales/en.ftl
@@ -248,7 +248,7 @@ chat-stop-failed = Could not stop · { $error }
chat-send-failed = Not accepted · Draft kept · { $error }
chat-send-unknown = Outcome unknown · Ctrl+R to check · { $id } · { $error }
chat-reconcile = Check delivery without resending · Ctrl+R
-chat-retry-original = Retry original message (same ID and text)
+chat-retry-original = Retry original message
chat-send-unknown-retry = Outcome unknown · Ctrl+R checks without resending. Commands can retry the original message, not your newer edits. { $id } · { $error }
chat-checking = Checking delivery…
chat-send-unconfirmed = Not yet confirmed · Draft kept; Ctrl+R to check again · { $id }
diff --git a/crates/tui/locales/zh-CN.ftl b/crates/tui/locales/zh-CN.ftl
index 6b987aa862..9b005a05cb 100644
--- a/crates/tui/locales/zh-CN.ftl
+++ b/crates/tui/locales/zh-CN.ftl
@@ -248,7 +248,7 @@ chat-stop-failed = 未能中止 · { $error }
chat-send-failed = 未被接收 · 草稿已保留 · { $error }
chat-send-unknown = 结果未知 · Ctrl+R 核对 · { $id } · { $error }
chat-reconcile = 核对发送结果,不重发 · Ctrl+R
-chat-retry-original = 重试原消息(原 ID 和内容)
+chat-retry-original = 重试原消息
chat-send-unknown-retry = 结果未知 · Ctrl+R 只核对;可从命令面板重试原消息,不会发送之后的新编辑。{ $id } · { $error }
chat-checking = 正在核对发送结果…
chat-send-unconfirmed = 尚未确认 · 草稿已保留;Ctrl+R 再次核对 · { $id }
diff --git a/crates/tui/locales/zh-TW.ftl b/crates/tui/locales/zh-TW.ftl
index 95e98aa249..521289477d 100644
--- a/crates/tui/locales/zh-TW.ftl
+++ b/crates/tui/locales/zh-TW.ftl
@@ -248,7 +248,7 @@ chat-stop-failed = 未能中止 · { $error }
chat-send-failed = 未被接受 · 草稿已保留 · { $error }
chat-send-unknown = 結果未知 · Ctrl+R 核對 · { $id } · { $error }
chat-reconcile = 核對傳送結果,不重送 · Ctrl+R
-chat-retry-original = 重試原訊息(原 ID 與內容)
+chat-retry-original = 重試原訊息
chat-send-unknown-retry = 結果未知 · Ctrl+R 僅核對;可從命令面板重試原訊息,不會傳送之後的新編輯。{ $id } · { $error }
chat-checking = 正在核對傳送結果…
chat-send-unconfirmed = 尚未確認 · 草稿已保留;Ctrl+R 再次核對 · { $id }
diff --git a/crates/tui/src/pages/sending.rs b/crates/tui/src/pages/sending.rs
index 61d9a5f2ea..1207023220 100644
--- a/crates/tui/src/pages/sending.rs
+++ b/crates/tui/src/pages/sending.rs
@@ -23,18 +23,20 @@ use maka_client::{ClientError, RequestFailure};
use maka_protocol::{
OperationErrorCode,
message::{ExecutionResolution, Placement, SubmitInput, SubmitResult},
- turn::MessageContent,
+ turn::{MessageContent, TurnOrchestration},
};
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Submission {
pub root_id: String,
pub origin_epoch: String,
pub session: String,
pub id: String,
- pub text: String,
+ pub content: MessageContent,
pub placement: Placement,
+ pub input_selections: std::collections::BTreeMap<String, Vec<String>>,
+ pub turn_orchestration: Option<TurnOrchestration>,
}
pub enum Delivery {
Pending,
@@ -64,17 +66,10 @@ impl Submission {
origin_host_epoch: self.origin_epoch.clone(),
session_id: self.session.clone(),
message_id: self.id.clone(),
- content: MessageContent {
- text: self.text.clone(),
- display_text: None,
- attachments: None,
- directory_references: None,
- quotes: None,
- inline_references: None,
- },
+ content: self.content.clone(),
placement: self.placement,
- input_selections: Default::default(),
- turn_orchestration: None,
+ input_selections: self.input_selections.clone(),
+ turn_orchestration: self.turn_orchestration.clone(),
}
}
}
@@ -143,8 +138,17 @@ impl App {
origin_epoch: epoch.clone(),
session: session.clone(),
id: uuid::Uuid::new_v4().to_string(),
- text,
+ content: MessageContent {
+ text,
+ display_text: None,
+ attachments: None,
+ directory_references: None,
+ quotes: None,
+ inline_references: None,
+ },
placement,
+ input_selections: Default::default(),
+ turn_orchestration: None,
};
self.sending.insert(
session,
@@ -167,7 +171,7 @@ impl App {
let Some(sent) = self.sending.get_mut(&request.session) else {
return;
};
- if sent.request.id != request.id || sent.request.root_id != request.root_id {
+ if sent.request != request {
return;
}
let retrying = matches!(sent.delivery, Delivery::Retrying);
@@ -181,7 +185,7 @@ impl App {
Ok(SubmitResult::Blocked { message, .. }) => Delivery::Failed(message),
Ok(_) => {
if let Some(editor) = self.drafts.get_mut(&request.session) {
- editor.clear_if_unchanged(&request.text);
+ editor.clear_if_unchanged(&request.content.text);
}
Delivery::Accepted
}
@@ -227,16 +231,13 @@ impl App {
let Some(sent) = self.sending.get_mut(&request.session) else {
return;
};
- if sent.request.id != request.id
- || sent.request.root_id != request.root_id
- || !matches!(sent.delivery, Delivery::Checking)
- {
+ if sent.request != request || !matches!(sent.delivery, Delivery::Checking) {
return;
}
sent.delivery = match result {
Ok(Some(ExecutionResolution::Pending { .. } | ExecutionResolution::Owned { .. })) => {
if let Some(editor) = self.drafts.get_mut(&request.session) {
- editor.clear_if_unchanged(&request.text);
+ editor.clear_if_unchanged(&request.content.text);
}
Delivery::Accepted
}
diff --git a/crates/tui/src/state.rs b/crates/tui/src/state.rs
index 4344827b68..4a45e4229f 100644
--- a/crates/tui/src/state.rs
+++ b/crates/tui/src/state.rs
@@ -19,6 +19,7 @@
mod snapshot;
mod store;
+mod submission;
use crate::{
app::App,
diff --git a/crates/tui/src/state/snapshot.rs b/crates/tui/src/state/snapshot.rs
index c9476b1627..1cdafabc0e 100644
--- a/crates/tui/src/state/snapshot.rs
+++ b/crates/tui/src/state/snapshot.rs
@@ -22,7 +22,7 @@ use crate::{
editor::{Editor, saved::Saved},
i18n::LocalePreference,
navigation::{Route, tabs::LIMIT},
- pages::sending::{Delivery, Sending, Submission},
+ pages::sending::{Delivery, Sending},
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet};
@@ -35,7 +35,7 @@ pub struct Snapshot {
route: Route,
tabs: Vec<String>,
drafts: BTreeMap<String, Saved>,
- unresolved: Vec<Submission>,
+ unresolved: Vec<super::submission::Saved>,
locale: LocalePreference,
terminal_colors: bool,
#[serde(default)]
@@ -66,7 +66,7 @@ impl Snapshot {
.collect();
unresolved.sort_by(|left, right| left.session.cmp(&right.session));
Self {
- version: 6,
+ version: 7,
root: root.into(),
route: app.navigation.current(),
tabs: app.tabs.entries.iter().map(|tab| tab.id.clone()).collect(),
@@ -75,7 +75,10 @@ impl Snapshot {
.iter()
.map(|(id, editor)| (id.clone(), editor.save()))
.collect(),
- unresolved,
+ unresolved: unresolved
+ .into_iter()
+ .map(|request| super::submission::Saved::Current(Box::new(request)))
+ .collect(),
locale: app.i18n.preference,
terminal_colors: app.theme.choice == crate::theme::Choice::Terminal,
theme: Some(app.theme.choice),
@@ -95,7 +98,7 @@ impl Snapshot {
let id = |id: &str| {
!id.is_empty() && id.encode_utf16().count() <= 256 && !id.chars().any(char::is_control)
};
- if !matches!(self.version, 1..=6)
+ if !matches!(self.version, 1..=7)
|| self.root != root
|| self.tabs.len() > LIMIT
|| self.drafts.len() > LIMIT
@@ -158,10 +161,10 @@ impl Snapshot {
}
let mut seen = HashSet::new();
for request in &self.unresolved {
+ let request = request.clone().request(self.version)?;
if request.root_id != root
|| !self.drafts.contains_key(&request.session)
- || !seen.insert(&request.session)
- || request.text.len() > 256 * 1024
+ || !seen.insert(request.session.clone())
{
return Err("Invalid saved submission identity".into());
}
@@ -191,6 +194,7 @@ impl Snapshot {
app.drafts.insert(id, Editor::restore(saved)?);
}
for request in self.unresolved {
+ let request = request.request(self.version)?;
app.sending.insert(
request.session.clone(),
Sending {
@@ -247,6 +251,15 @@ mod tests {
};
app
}
+ fn text_only_requests(value: &mut serde_json::Value) {
+ for request in value["unresolved"].as_array_mut().unwrap() {
+ let fields = request.as_object_mut().unwrap();
+ let content = fields.remove("content").unwrap();
+ fields.insert("text".into(), content["text"].clone());
+ fields.remove("input_selections");
+ fields.remove("turn_orchestration");
+ }
+ }
#[test]
fn checkpoint_restores_drafts_preferences_and_original_unknown_identity_without_dispatching() {
let mut original = app();
@@ -300,7 +313,7 @@ mod tests {
assert!(restored.retry_submission().is_none());
assert!(restored.reconciliation().is_some());
let mut invalid: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
- invalid["version"] = serde_json::json!(7);
+ invalid["version"] = serde_json::json!(8);
assert!(
serde_json::from_value::<Snapshot>(invalid)
.unwrap()
@@ -308,6 +321,7 @@ mod tests {
.is_err()
);
let mut legacy: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
+ text_only_requests(&mut legacy);
legacy.as_object_mut().unwrap().remove("theme");
legacy["version"] = serde_json::json!(1);
legacy.as_object_mut().unwrap().remove("readings");
@@ -316,6 +330,13 @@ mod tests {
let legacy: Snapshot = serde_json::from_value(legacy).unwrap();
legacy.validate("root").unwrap();
assert!(legacy.readings.is_empty());
+ let mut migrated = app();
+ legacy.restore(&mut migrated, false).unwrap();
+ assert_eq!(migrated.sending["a"].request, request);
+ assert!(matches!(
+ migrated.sending["a"].delivery,
+ Delivery::Unknown(None)
+ ));
// v4 adds only the original OAuth query basis. A v3 reader must not
// silently discard it; current readers still accept v1-v3 without it.
@@ -334,6 +355,7 @@ mod tests {
oauth["oauth"]
);
oauth.as_object_mut().unwrap().remove("theme");
+ text_only_requests(&mut oauth);
oauth["version"] = serde_json::json!(3);
assert!(
serde_json::from_value::<Snapshot>(oauth.clone())
@@ -348,6 +370,104 @@ mod tests {
.unwrap();
}
+ #[test]
+ fn structured_unknown_requests_keep_all_content_and_intent_across_restore_and_retry() {
+ use maka_protocol::message::{Placement, SubmitResult};
+ use serde_json::json;
+
+ let mut original = app();
+ original.apply(Action::Visit(Route::Session("a".into())));
+ original
+ .drafts
+ .get_mut("a")
+ .unwrap()
+ .insert("Original prompt");
+ let mut request = original.submission_for(Placement::CurrentTurn).unwrap();
+ request.content = serde_json::from_value(json!({
+ "text":"Original prompt", "displayText":"🦀 @src/main.rs",
+ "attachments":[{"kind":"image","name":"image.png","mimeType":"image/png","bytes":42,
+ "ref":{"kind":"session_file","sessionId":"source","relativePath":"image.png"}}],
+ "quotes":[{"text":"Original quotation","label":"Source","sourceTurnId":"source-turn"}],
+ "directoryReferences":[{"hostId":"origin-host","path":"/original/workspace"}],
+ "inlineReferences":[{"kind":"workspace_file","value":"@src/main.rs","label":"main.rs","start":3}]
+ })).unwrap();
+ request
+ .input_selections
+ .insert("skills".into(), vec!["review".into()]);
+ request.turn_orchestration =
+ Some(serde_json::from_value(json!({"mode":"code","source":"slash_command"})).unwrap());
+ request.input().validate().unwrap();
+ original.sending.get_mut("a").unwrap().request = request.clone();
+ let saved = serde_json::to_value(Snapshot::capture(&original, "root")).unwrap();
+ assert_eq!(saved["version"], 7);
+ let mut restored = app();
+ serde_json::from_value::<Snapshot>(saved.clone())
+ .unwrap()
+ .restore(&mut restored, false)
+ .unwrap();
+ assert_eq!(restored.sending["a"].request.input(), request.input());
+ assert!(matches!(
+ restored.sending["a"].delivery,
+ Delivery::Unknown(None)
+ ));
+ let retry = restored.retry_submission().unwrap();
+ assert_eq!(retry, request);
+
+ // Same message ID is not enough: an altered attachment, display or
+ // preparation intent must not release the saved request or its draft.
+ let mut changed = retry.clone();
+ changed.content.attachments.as_mut().unwrap()[0].name = "other.png".into();
+ assert!(!restored.after_checkpoint(&changed, &Ok(())));
+ restored.submitted(
+ changed.clone(),
+ Ok(SubmitResult::Blocked {
+ message: "foreign reply".into(),
+ preparation: vec![],
+ }),
+ );
+ assert!(matches!(restored.sending["a"].delivery, Delivery::Retrying));
+ assert!(restored.after_checkpoint(&retry, &Ok(())));
+ restored.abandon_pending_submissions();
+ let checking = restored.reconciliation().unwrap();
+ let accepted = maka_protocol::message::ExecutionResolution::Pending {
+ message_id: retry.id.clone(),
+ };
+ restored.reconciled(changed, Ok(Some(accepted.clone())));
+ assert!(matches!(restored.sending["a"].delivery, Delivery::Checking));
+ assert_eq!(restored.drafts["a"].text(), "Original prompt");
+ restored.reconciled(checking, Ok(Some(accepted)));
+ assert!(restored.drafts["a"].text().is_empty());
+
+ for (pointer, value) in [
+ ("/version", json!(6)),
+ ("/unresolved/0/placement", json!("next_turn")),
+ ("/unresolved/0/content/inlineReferences/0/start", json!(2)),
+ (
+ "/unresolved/0/content/attachments/0/ref",
+ json!({"kind":"session_context","sessionId":"source","refId":"owned"}),
+ ),
+ ] {
+ let mut invalid = saved.clone();
+ *invalid.pointer_mut(pointer).unwrap() = value;
+ assert!(
+ serde_json::from_value::<Snapshot>(invalid)
+ .unwrap()
+ .validate("root")
+ .is_err(),
+ "{pointer}"
+ );
+ }
+ let mut incomplete = saved;
+ incomplete["unresolved"][0]
+ .as_object_mut()
+ .unwrap()
+ .remove("input_selections");
+ assert!(
+ serde_json::from_value::<Snapshot>(incomplete).is_err(),
+ "never downgrade an incomplete structured request to text-only"
+ );
+ }
+
#[test]
fn navigation_checkpoint_restores_both_branches_and_safe_focus_but_not_live_controls() {
let mut original = app();
diff --git a/crates/tui/src/state/submission.rs b/crates/tui/src/state/submission.rs
new file mode 100644
index 0000000000..1d89473f8f
--- /dev/null
+++ b/crates/tui/src/state/submission.rs
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::pages::sending::Submission;
+use maka_protocol::{message::Placement, turn::MessageContent};
+use serde::{Deserialize, Serialize};
+
+/// v1-v6 saved text-only requests. Never reinterpret an incomplete v7 request
+/// as legacy: losing metadata would change an unknown request's identity.
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub(super) enum Saved {
+ Current(Box<Submission>),
+ Legacy(Legacy),
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub(super) struct Legacy {
+ root_id: String,
+ origin_epoch: String,
+ session: String,
+ id: String,
+ text: String,
+ placement: Placement,
+}
+
+impl Saved {
+ pub fn request(self, version: u32) -> Result<Submission, String> {
+ match self {
+ Self::Current(request) if version == 7 => Ok(*request),
+ Self::Legacy(request) if (1..=6).contains(&version) => Ok(Submission {
+ root_id: request.root_id,
+ origin_epoch: request.origin_epoch,
+ session: request.session,
+ id: request.id,
+ content: MessageContent {
+ text: request.text,
+ display_text: None,
+ attachments: None,
+ directory_references: None,
+ quotes: None,
+ inline_references: None,
+ },
+ placement: request.placement,
+ input_selections: Default::default(),
+ turn_orchestration: None,
+ }),
+ _ => Err("Submission does not match checkpoint version".into()),
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org