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/10/13 06:02:20 UTC
(maka) branch feat/runtime-host-rust updated: fix(tui): preserve history reading and composer choices
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 c143e7e67f fix(tui): preserve history reading and composer choices
c143e7e67f is described below
commit c143e7e67f92787ab52d16043dd5cc4014f69195
Author: Wang <m4...@gmail.com>
AuthorDate: Tue Oct 13 14:02:02 2026 +0800
fix(tui): preserve history reading and composer choices
---
platform/render-terminal/src/application/run.rs | 32 +++-
.../src/application/tests/layout.rs | 9 +
.../src/application/tests/reading.rs | 17 ++
plugins/agent/agent-screen/component-events.ts | 35 ++--
plugins/agent/agent-screen/conversation-view.ts | 5 +-
plugins/agent/agent-screen/frame.ts | 4 +-
plugins/agent/agent-screen/messages.ts | 37 +++-
plugins/agent/agent-screen/results.ts | 25 ++-
plugins/agent/agent-screen/state.ts | 1 +
plugins/agent/agent-screen/task-overview.ts | 13 +-
plugins/settings/models/index.ts | 31 ++--
plugins/shared/attachments.ts | 6 +-
plugins/shared/components.ts | 17 +-
plugins/shared/tool-cards.ts | 1 +
products/maka/bundle/tests/connections.test.mjs | 64 +++++++
products/maka/bundle/tests/task-workbench.test.mjs | 190 +++++++++++++++++++++
products/maka/bundle/tests/timeline.test.mjs | 104 +++++++++++
products/maka/cli/notices.json | 22 +--
18 files changed, 545 insertions(+), 68 deletions(-)
diff --git a/platform/render-terminal/src/application/run.rs b/platform/render-terminal/src/application/run.rs
index dbb87b0882..7590ef0ac7 100644
--- a/platform/render-terminal/src/application/run.rs
+++ b/platform/render-terminal/src/application/run.rs
@@ -78,6 +78,7 @@ pub fn run(
}
}
let mut exit_key = ExitKey::default();
+ let mut queued_input = None;
loop {
if failure.is_some()
|| ((!presenter.surface.is_live()
@@ -119,6 +120,7 @@ pub fn run(
// original root. A late receipt cannot close a replacement.
screen.take();
exit_key = ExitKey::default();
+ queued_input = None;
let cleanup = guard.finish().map_err(|e| sanitize(&e.to_string()));
if let Err(error) = presenter.surface_closed(&original, cleanup) {
presenter.report(&error);
@@ -165,8 +167,13 @@ pub fn run(
}
presenter.present(scene.expect("successful paint"));
}
- if event::poll(Duration::from_millis(16)).map_err(|e| e.to_string())? {
- let input = event::read().map_err(|e| e.to_string())?;
+ if queued_input.is_some()
+ || event::poll(Duration::from_millis(16)).map_err(|e| e.to_string())?
+ {
+ let input = match queued_input.take() {
+ Some(input) => input,
+ None => event::read().map_err(|e| e.to_string())?,
+ };
if exit_key.ready(Some(&input), Instant::now()) {
return Ok(true);
}
@@ -190,7 +197,24 @@ pub fn run(
Instant::now(),
));
}
+ let scrolling = is_wheel(&input);
presenter.input(input).map_err(|e| e.to_string())?;
+ // A trackpad gesture arrives as many small wheel events. Apply
+ // the queued gesture before another live update and repaint,
+ // so streaming cannot repeatedly move the tail ahead of it.
+ // Keep other gestures for the next presented frame.
+ let until = Instant::now() + Duration::from_millis(8);
+ while scrolling
+ && Instant::now() < until
+ && event::poll(Duration::ZERO).map_err(|e| e.to_string())?
+ {
+ let input = event::read().map_err(|e| e.to_string())?;
+ if !is_wheel(&input) {
+ queued_input = Some(input);
+ break;
+ }
+ presenter.input(input).map_err(|e| e.to_string())?;
+ }
if let Err(error) = presenter.flush_clipboard(&mut std::io::stdout().lock()) {
presenter.report(&error);
}
@@ -233,6 +257,10 @@ pub fn run(
std::thread::sleep(Duration::from_millis(10));
}
}
+fn is_wheel(event: &Event) -> bool {
+ matches!(event, Event::Mouse(mouse) if matches!(mouse.kind,
+ crossterm::event::MouseEventKind::ScrollUp | crossterm::event::MouseEventKind::ScrollDown))
+}
fn is_copy(event: &Event) -> bool {
matches!(event,Event::Key(key) if key.kind!=KeyEventKind::Release && matches!(key.code,KeyCode::Char('c'|'C')) && key.modifiers==KeyModifiers::CONTROL|KeyModifiers::SHIFT)
}
diff --git a/platform/render-terminal/src/application/tests/layout.rs b/platform/render-terminal/src/application/tests/layout.rs
index 7a96a34b5b..52b8761055 100644
--- a/platform/render-terminal/src/application/tests/layout.rs
+++ b/platform/render-terminal/src/application/tests/layout.rs
@@ -922,6 +922,15 @@ fn follow_end_uses_actual_flush_and_manual_interaction_survives_recipe_updates()
refresh(&mut presenter);
flushed(&mut presenter, 10, 5);
assert_eq!(scroll_state(&presenter, "body"), (3, 3));
+ // One queued gesture is applied against the same presented frame, then
+ // streaming extends the content before its next paint.
+ wheel(&mut presenter, false, 1, 1);
+ wheel(&mut presenter, false, 1, 1);
+ source.borrow_mut()["children"][0]["children"][0]["text"] =
+ json!("0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13");
+ refresh(&mut presenter);
+ flushed(&mut presenter, 10, 5);
+ assert_eq!(scroll_state(&presenter, "body"), (0, 0));
assert!(Rc::ptr_eq(
&owner,
&presenter.resources.owners[&presenter.root.id()].identity
diff --git a/platform/render-terminal/src/application/tests/reading.rs b/platform/render-terminal/src/application/tests/reading.rs
index c1647033a1..cd44b25edf 100644
--- a/platform/render-terminal/src/application/tests/reading.rs
+++ b/platform/render-terminal/src/application/tests/reading.rs
@@ -93,6 +93,23 @@ fn close(presenter: &mut Presenter) {
presenter.shutdown().unwrap();
}
#[test]
+fn wheel_above_a_short_page_reports_the_original_reading_boundary() {
+ let (_runtime, mut presenter, _) = native(vec![source("one", "first")]);
+ draw(&mut presenter, 30, 12);
+ mouse(&mut presenter, MouseEventKind::ScrollUp, 10, 8);
+ draw(&mut presenter, 30, 12);
+ presenter.sync_native().unwrap();
+ let observed = observation(&presenter);
+ assert_eq!(observed["scroll"]["direction"], "up");
+ assert_eq!(observed["viewport"]["atStart"], true);
+ assert_eq!(observed["viewport"]["atEnd"], true);
+ assert_eq!(
+ observed["viewport"]["readingRevision"],
+ observed["readingRevision"]
+ );
+ close(&mut presenter);
+}
+#[test]
fn selected_child_follows_original_native_focus_and_only_committed_group_geometry() {
let group = |key: &str, child| json!({"kind":"stack","key":key,"direction":"column","gap":0,"padding":0,"background":0,"children":[child]});
let (_runtime, mut presenter, props) = native(vec![
diff --git a/plugins/agent/agent-screen/component-events.ts b/plugins/agent/agent-screen/component-events.ts
index 2d11e7781c..755ccac709 100644
--- a/plugins/agent/agent-screen/component-events.ts
+++ b/plugins/agent/agent-screen/component-events.ts
@@ -777,23 +777,26 @@ export function handleComponent<N extends Node>(
};
readHistorySource(state, actions, { kind: 'start' });
}
- } else if (
- input.name === 'older' &&
- page.before &&
- !Object.values(state.pending).some(
- (request) => request.kind === 'history' && request.epoch === state.epoch,
+ } else if (input.name === 'older' && page.before) {
+ // A short page is both at its start and end. Asking for older content
+ // must suspend following even while that native viewport still fits.
+ const visit = state.visits.find((entry) => entry.id === state.selected?.id);
+ if (visit) visit.following = false;
+ if (
+ !Object.values(state.pending).some(
+ (request) => request.kind === 'history' && request.epoch === state.epoch,
+ )
)
- )
- history(
- state,
- actions,
- { kind: 'before', cursor: page.before },
- page.through,
- false,
- false,
- target === 'tui',
- );
- else if (
+ history(
+ state,
+ actions,
+ { kind: 'before', cursor: page.before },
+ page.through,
+ false,
+ false,
+ target === 'tui',
+ );
+ } else if (
input.name === 'newer' &&
page.after &&
!Object.values(state.pending).some(
diff --git a/plugins/agent/agent-screen/conversation-view.ts b/plugins/agent/agent-screen/conversation-view.ts
index f8797ef181..1a54087bec 100644
--- a/plugins/agent/agent-screen/conversation-view.ts
+++ b/plugins/agent/agent-screen/conversation-view.ts
@@ -126,7 +126,10 @@ function readingProps(
)
.flatMap((entry) => (entry.toolUseId === null ? [] : [entry.toolUseId]))
: [],
- live: !!state.run && !!state.window?.latest,
+ live:
+ !!state.run &&
+ !!state.window?.latest &&
+ state.visits.find((visit) => visit.id === state.selected?.id)?.following !== false,
};
}
diff --git a/plugins/agent/agent-screen/frame.ts b/plugins/agent/agent-screen/frame.ts
index d3434fd63b..bede4bf041 100644
--- a/plugins/agent/agent-screen/frame.ts
+++ b/plugins/agent/agent-screen/frame.ts
@@ -589,9 +589,9 @@ export function frame<N extends Node>(
state.dialog === 'terminal' ? { width: pixels(900) } : {},
state.dialog === 'attachments'
? {
- width: 64,
+ width: state.attachments.selection?.browse ? 80 : 64,
height: state.attachments.selection?.browse
- ? 20
+ ? 28
: Math.min(20, attached.length * 3 + 7),
}
: {},
diff --git a/plugins/agent/agent-screen/messages.ts b/plugins/agent/agent-screen/messages.ts
index 8e82b1a23e..82280b05fd 100644
--- a/plugins/agent/agent-screen/messages.ts
+++ b/plugins/agent/agent-screen/messages.ts
@@ -39,7 +39,6 @@ export function appendLive(
invocationId: string,
text: string,
) {
- if (state.visits.find((visit) => visit.id === state.selected?.id)?.following === false) return;
const messageId = part.messageId;
const field = part.textKind === 'thinking' ? 'thinking' : 'body';
if (
@@ -50,6 +49,13 @@ export function appendLive(
)
)
return;
+ const previous = state.live.at(-1);
+ const adjacent =
+ previous?.closedAt === null &&
+ previous.messageId === messageId &&
+ previous.invocationId === invocationId &&
+ previous.stepId === part.stepId &&
+ previous.field === field;
const ids = new Set(state.live.map((fragment) => fragment.messageId));
let remaining = 16000 - state.live.reduce((total, fragment) => total + fragment.text.length, 0);
// Thinking shares the existing prefix budget but leaves room for an answer
@@ -57,18 +63,30 @@ export function appendLive(
if (field === 'thinking') {
const thinking = state.live.filter((fragment) => fragment.field === 'thinking');
const thinkingIds = new Set(thinking.map((fragment) => fragment.messageId));
- if (thinking.length >= 32 || (!thinkingIds.has(messageId) && thinkingIds.size >= 8)) return;
+ if (
+ (!adjacent && thinking.length >= 32) ||
+ (!thinkingIds.has(messageId) && thinkingIds.size >= 8)
+ )
+ return;
remaining = Math.min(
remaining,
8000 - thinking.reduce((total, fragment) => total + fragment.text.length, 0),
);
}
// A truncated surrogate can leave one unused code unit; keep the prefix closed.
- if (remaining <= 1 || state.live.length >= 64 || (!ids.has(messageId) && ids.size >= 16)) return;
+ if (
+ remaining <= 1 ||
+ (!adjacent && state.live.length >= 64) ||
+ (!ids.has(messageId) && ids.size >= 16)
+ )
+ return;
text = text.slice(0, remaining);
const last = text.charCodeAt(text.length - 1);
if (last >= 0xd800 && last <= 0xdbff) text = text.slice(0, -1);
- if (text)
+ if (text && adjacent) {
+ previous.text += text;
+ previous.sequence = sequence;
+ } else if (text)
state.live.push({
sequence,
messageId,
@@ -90,9 +108,16 @@ export function closeLive(state: State, sequence: number, invocationId: string,
fragment.closedAt = sequence;
}
+function chunkId(chunk: NonNullable<State['window']>['page']['chunks'][number]) {
+ return (
+ (chunk.field === 'thinking' ? 'thinking-' + chunk.messageId : chunk.messageId) +
+ (chunk.offset ? '-offset-' + chunk.offset : '')
+ );
+}
+
export function messages(state: State): Message[] {
const canonical: Message[] = (state.window?.page.chunks ?? []).map((chunk) => ({
- id: chunk.field === 'thinking' ? 'thinking-' + chunk.messageId : chunk.messageId,
+ id: chunkId(chunk),
contentKey: `canonical-${chunk.sequence}-${chunk.offset}${chunk.field === 'thinking' ? '-thinking' : ''}`,
role: chunk.field === 'thinking' ? 'thinking' : chunk.role,
timestamp: chunk.timestamp,
@@ -143,7 +168,7 @@ export function turnTimings(state: State, displayed: Message[]): TimelineProps['
turns.set(chunk.turnId, {
...chunk.timing,
turnId: chunk.turnId,
- afterMessageId: chunk.field === 'thinking' ? 'thinking-' + chunk.messageId : chunk.messageId,
+ afterMessageId: chunkId(chunk),
});
}
if (state.window?.latest && state.run) {
diff --git a/plugins/agent/agent-screen/results.ts b/plugins/agent/agent-screen/results.ts
index 33550a72b7..f3a042b012 100644
--- a/plugins/agent/agent-screen/results.ts
+++ b/plugins/agent/agent-screen/results.ts
@@ -363,6 +363,7 @@ export function querySucceeded(state: State, actions: Action[], request: Request
) {
state.run = observation.run ?? null;
state.runThrough = observation.through;
+ if (!state.run && state.notice === 'stopping') state.notice = null;
}
if (!state.watch)
state.parts = observation.parts.map((part) => ({
@@ -475,6 +476,11 @@ export function querySucceeded(state: State, actions: Action[], request: Request
}
case 'history': {
if (request.epoch !== state.epoch || request.navigation !== state.navigation) break;
+ if (
+ request.implicit &&
+ state.visits.find((visit) => visit.id === state.selected?.id)?.following === false
+ )
+ break;
const page = value as AgentHistoryPage;
if (page.through !== request.through) {
queryFailed(state, request);
@@ -513,12 +519,19 @@ export function querySucceeded(state: State, actions: Action[], request: Request
break;
}
const old = state.window;
- const splitMessage = old?.page.chunks.some((oldChunk) =>
- page.chunks.some(
- (chunk) => chunk.sequence === oldChunk.sequence && chunk.field === oldChunk.field,
- ),
- );
- if (request.extend && old?.page.through === page.through && !splitMessage) {
+ if (request.extend && !page.chunks.length) {
+ const position =
+ request.position.kind === 'before' && page.before
+ ? { kind: 'before' as const, cursor: page.before }
+ : request.position.kind === 'after' && page.after
+ ? { kind: 'after' as const, cursor: page.after }
+ : null;
+ if (position) {
+ history(state, actions, position, page.through, request.latest, false, true);
+ break;
+ }
+ }
+ if (request.extend && old?.page.through === page.through) {
const before = request.position.kind === 'before';
const chunks = before
? [...page.chunks, ...old.page.chunks]
diff --git a/plugins/agent/agent-screen/state.ts b/plugins/agent/agent-screen/state.ts
index b2e1337732..026add76ea 100644
--- a/plugins/agent/agent-screen/state.ts
+++ b/plugins/agent/agent-screen/state.ts
@@ -197,6 +197,7 @@ export interface Visit {
session: AgentSession;
epoch: number;
progress: {
+ turnId: string | null;
through: number;
goal: string;
action: string;
diff --git a/plugins/agent/agent-screen/task-overview.ts b/plugins/agent/agent-screen/task-overview.ts
index 2db7ed7ced..9de6e42977 100644
--- a/plugins/agent/agent-screen/task-overview.ts
+++ b/plugins/agent/agent-screen/task-overview.ts
@@ -29,17 +29,22 @@ import type { State, Visit } from './state.js';
// replace current progress, and an ended turn never claims a verified delivery.
export function taskProgress(state: State, locale: string): Visit['progress'] {
const visit = state.visits.find((entry) => entry.id === state.selected?.id);
- const previous = visit?.progress ?? null;
+ const page = state.window?.page;
+ const turnId =
+ state.run?.turnId ??
+ (state.window?.latest ? page?.chunks.at(-1)?.turnId : null) ??
+ visit?.progress?.turnId ??
+ null;
+ const previous = visit?.progress?.turnId === turnId ? visit.progress : null;
const queuedGoal = state.queue?.entries.find((entry) => entry.kind === 'in_flight')?.preview;
if (queuedGoal && previous) previous.goal = compactLabel(queuedGoal, 100);
- const page = state.window?.page;
if (!page || !state.window?.latest || page.through < (previous?.through ?? 0)) return previous;
- const last = page.chunks.at(-1);
- const turn = last ? page.chunks.filter((chunk) => chunk.turnId === last.turnId) : [];
+ const turn = page.chunks.filter((chunk) => chunk.turnId === turnId);
const goal = turn.find((chunk) => chunk.role === 'user' && chunk.field === 'body');
const tool = [...turn].reverse().find((chunk) => chunk.tool);
const timing = [...turn].reverse().find((chunk) => chunk.timing)?.timing;
const progress = {
+ turnId,
through: page.through,
goal: queuedGoal
? compactLabel(queuedGoal, 100)
diff --git a/plugins/settings/models/index.ts b/plugins/settings/models/index.ts
index 970e812beb..4dca0301a6 100644
--- a/plugins/settings/models/index.ts
+++ b/plugins/settings/models/index.ts
@@ -61,6 +61,7 @@ interface State {
reveal: ReadingCommand | null;
open: boolean;
draft: SelectedModel | null;
+ thinkingByModel: Record<string, ThinkingLevel | null>;
thinkingOpen: boolean;
choices: SettingsModelChoice[];
windowStart: number;
@@ -111,11 +112,10 @@ function sameModel(a: SettingsModelChoice, b: SettingsModelChoice) {
);
}
function selectDraft(state: State, choice: SettingsModelChoice) {
- const prior = state.draft;
- const same =
- prior?.choice.selection.connectionId === choice.selection.connectionId &&
- prior.choice.selection.modelId === choice.selection.modelId;
- const selection = { choice, thinkingLevel: same ? prior.thinkingLevel : null };
+ const selection = {
+ choice,
+ thinkingLevel: state.thinkingByModel[modelKey(state, choice)] ?? null,
+ };
state.draft = selection;
state.active = state.choices.findIndex((row) => sameModel(row, choice));
state.thinkingOpen = false;
@@ -124,6 +124,9 @@ function open(state: State, props: ModelsProps) {
state.open = true;
state.thinkingOpen = false;
state.draft = state.staleSelection ? null : props.selection;
+ state.thinkingByModel = state.draft
+ ? { [modelKey(state, state.draft.choice)]: state.draft.thinkingLevel }
+ : {};
const choice = state.draft
? state.choices.find((row) => sameModel(row, state.draft!.choice))
: state.choices[0];
@@ -242,6 +245,7 @@ function close(state: State) {
state.open = false;
state.thinkingOpen = false;
state.draft = null;
+ state.thinkingByModel = {};
if (state.pending?.kind === 'text') state.pending.choice = null;
}
function frame<N extends Node>(
@@ -259,7 +263,11 @@ function frame<N extends Node>(
state.publishedSetup = state.setup;
}
const chosen = state.staleSelection ? null : props.selection?.choice;
- const trigger = ui.button('toggle', chosen ? chosen.label : text.chooseModel, props.busy, false, {
+ const effort = props.selection?.thinkingLevel;
+ const label = chosen
+ ? chosen.label + (effort ? ` · ${text.thinking} ${text[thinkingCopy[effort]]}` : '')
+ : text.chooseModel;
+ const trigger = ui.button('toggle', label, props.busy, false, {
width: fill,
minWidth: 0,
justify: 'start',
@@ -623,6 +631,7 @@ export function models<N extends Node>(
reveal: null,
open: false,
draft: null,
+ thinkingByModel: {},
thinkingOpen: false,
choices: [],
windowStart: 0,
@@ -894,6 +903,8 @@ export function models<N extends Node>(
...state.draft,
thinkingLevel: value === 'default' ? null : (value as ThinkingLevel),
};
+ state.thinkingByModel[modelKey(state, state.draft.choice)] =
+ state.draft.thinkingLevel;
state.thinkingOpen = false;
}
} else if (input.key === 'default-refresh') actions.push(...readPage(state));
@@ -925,13 +936,7 @@ export function models<N extends Node>(
state.windowStart + 64 < state.choices.length
)
state.windowStart += 32;
- if (
- (input.event as { kind?: string }).kind === 'submit' &&
- state.draft &&
- supports(state.draft)
- )
- actions.push(...capture(state, state.draft));
- else if (state.next && state.active >= state.choices.length - 3)
+ if (state.next && state.active >= state.choices.length - 3)
actions.push(...read(state, true));
}
}
diff --git a/plugins/shared/attachments.ts b/plugins/shared/attachments.ts
index 99e131633f..7c6ec2bb87 100644
--- a/plugins/shared/attachments.ts
+++ b/plugins/shared/attachments.ts
@@ -292,11 +292,7 @@ export function attachmentView<N extends Node>(
ui.box(
'attachment-browser-view',
[
- ui.box(
- 'attachment-browser-body',
- [ui.text('attachment-help', text.attachmentHelp, 13, theme.muted), ...fileContent],
- { scroll: 'manual', terminal: { grow: 1 } },
- ),
+ ui.box('attachment-browser-body', fileContent, { gap: 0, terminal: { grow: 1 } }),
ui.box(
'attachment-browser-controls',
[
diff --git a/plugins/shared/components.ts b/plugins/shared/components.ts
index b77e8252fb..032b2b821e 100644
--- a/plugins/shared/components.ts
+++ b/plugins/shared/components.ts
@@ -1629,9 +1629,22 @@ export function components<N extends Node>(
observed.pendingCommand == null
) {
next.scrollRevision = observed.scroll.readingRevision;
- if (observed.scroll.direction === 'up' && observed.viewport.atStart && props.older)
+ const rows = toolCards(props.messages);
+ const edgeRows = Math.max(1, Math.min(2, Math.floor(rows.length / 2)));
+ const visible = new Set(observed.viewport.visibleChildKeys);
+ const visibleRow = ({ message }: (typeof rows)[number]) =>
+ visible.has('message-' + message.id) || visible.has('time-' + message.contentKey);
+ if (
+ observed.scroll.direction === 'up' &&
+ props.older &&
+ (observed.viewport.atStart || rows.slice(0, edgeRows).some(visibleRow))
+ )
out.events.push({ name: 'older', value: {} });
- else if (observed.scroll.direction === 'down' && observed.viewport.atEnd && props.newer)
+ else if (
+ observed.scroll.direction === 'down' &&
+ props.newer &&
+ (observed.viewport.atEnd || rows.slice(-edgeRows).some(visibleRow))
+ )
out.events.push({ name: 'newer', value: {} });
}
if (
diff --git a/plugins/shared/tool-cards.ts b/plugins/shared/tool-cards.ts
index fb0cdbb1cb..a126ee153d 100644
--- a/plugins/shared/tool-cards.ts
+++ b/plugins/shared/tool-cards.ts
@@ -78,6 +78,7 @@ export function toolCards(
.map((message) => ({
message:
message.tool?.call &&
+ bySequence.get(message.source!)?.length === 1 &&
(message.role === 'tool_call' ||
(!bySequence.has(message.tool.call.sequence) &&
(resultsByCall.get(message.tool.call.sequence)?.size ?? 0) === 1))
diff --git a/products/maka/bundle/tests/connections.test.mjs b/products/maka/bundle/tests/connections.test.mjs
index dddf1b52f3..ed7dea3130 100644
--- a/products/maka/bundle/tests/connections.test.mjs
+++ b/products/maka/bundle/tests/connections.test.mjs
@@ -70,6 +70,11 @@ async function client(name, platform, extra = {}) {
out = component.event(out.frame.state, input, props, {});
return this;
},
+ update(next) {
+ Object.assign(props, next);
+ out = component.update(out.frame.state, props, {});
+ return this;
+ },
raw(key, kind = 'submit') {
return this.event({ kind: 'raw', key, event: { kind } });
},
@@ -97,6 +102,65 @@ async function client(name, platform, extra = {}) {
};
}
for (const platform of ['tui', 'desktop']) {
+ test(`${platform} model and thinking edits stay open and survive navigation until applied`, async () => {
+ const choice = (id) => ({
+ selection: { connectionId: 'a', revision: 1, modelId: id },
+ connectionName: 'A',
+ label: id,
+ thinkingLevels: ['low', 'high'],
+ defaultThinkingLevel: 'low',
+ });
+ const a = choice('a'),
+ b = choice('b');
+ const selection = { choice: a, thinkingLevel: 'high' };
+ const c = await client('models', platform, {
+ catalogEpoch: 0,
+ openRequest: null,
+ selection,
+ busy: false,
+ });
+ const page = (choices) => ({
+ kind: 'ready',
+ revision: 3,
+ choices,
+ defaultModel: { kind: 'none' },
+ next: null,
+ });
+ c.reply(c.action('models'), page([a, b]));
+ c.query('model-query', 1, '').raw('toggle');
+ const key = (id) => c.state.choiceKeys[JSON.stringify(['a', id])];
+ c.raw(key('b'));
+ assert.equal(c.state.open, true, 'clicking a model leaves its thinking control available');
+ assert.equal(c.action('read'), undefined, 'row selection does not commit');
+ c.raw('thinking-low');
+ c.raw(key('a'), 'activate');
+ assert.equal(c.state.draft.thinkingLevel, 'high');
+ c.raw(key('b'), 'activate');
+ assert.equal(c.state.draft.thinkingLevel, 'low');
+ c.query('model-query', 2, 'a');
+ c.reply(c.action('read'), { text: 'a' });
+ c.reply(c.action('models'), page([a]));
+ assert.equal(c.state.draft.thinkingLevel, 'high', 'search preserves the applied effort');
+ c.query('model-query', 3, '');
+ c.reply(c.action('models'), page([a, b]));
+ c.raw(key('b')).raw('apply');
+ c.reply(c.action('read'), { text: '' });
+ const applied = c.out.events.find((event) => event.name === 'selected').value;
+ assert.equal(applied.thinkingLevel, 'low');
+ assert.equal(applied.choice.selection.modelId, 'b');
+ assert.equal(c.state.open, false);
+ c.update({ selection: applied }).raw('toggle');
+ c.raw(key('a'), 'activate').raw(key('b'), 'activate');
+ assert.equal(
+ c.state.draft.thinkingLevel,
+ 'low',
+ 'reopened selection survives keyboard navigation',
+ );
+ c.raw('thinking-high').raw('close').raw('toggle');
+ assert.equal(c.state.draft.thinkingLevel, 'low', 'cancel discards unapplied edits');
+ c.raw('thinking-default').raw(key('a'), 'activate').raw(key('b'), 'activate');
+ assert.equal(c.state.draft.thinkingLevel, null, 'explicit default survives navigation');
+ });
test(`${platform} provider search Enter selects its visible result and exposes anonymous endpoints`, async () => {
const c = await client('connection-form', platform);
const anonymous = {
diff --git a/products/maka/bundle/tests/task-workbench.test.mjs b/products/maka/bundle/tests/task-workbench.test.mjs
index f8d239a401..51ad1ec13c 100644
--- a/products/maka/bundle/tests/task-workbench.test.mjs
+++ b/products/maka/bundle/tests/task-workbench.test.mjs
@@ -359,6 +359,62 @@ test('returning to a streaming task preserves its visible prefix until latest is
assert.equal(c.action('history').input.through, 50);
});
+test('returning to a running task buffers every delta while preserving the reading position', () => {
+ const run = { invocationId: 'run-a', turnId: 'turn-1', state: 'running' };
+ const c = client()
+ .observe(1, run)
+ .history(page(1, [chunk(1, 'Goal', { role: 'user' })]));
+ const reader = c.reader();
+ c.select('Task B').observe(1).history(page(1, []));
+ c.select('Task A').observe(1, run);
+ const landing = structuredClone(c.state.landing);
+ const events = [
+ {
+ sequence: 2,
+ invocationId: 'run-a',
+ turnId: 'turn-1',
+ fact: {
+ kind: 'part_started',
+ messageId: 'stream-a',
+ stepId: 'step-a',
+ partId: 'text',
+ textKind: 'text',
+ },
+ },
+ ...Array.from({ length: 100 }, (_, i) => ({
+ sequence: i + 3,
+ invocationId: 'run-a',
+ turnId: 'turn-1',
+ fact: { kind: 'delta', stepId: 'step-a', partId: 'text', text: `片段${i}🙂 ` },
+ })),
+ ];
+ c.event({
+ kind: 'action',
+ key: c.state.watch.key,
+ event: {
+ status: 'updated',
+ value: {
+ kind: 'events',
+ through: 102,
+ nextAfter: 102,
+ events,
+ },
+ },
+ });
+ assert.equal(c.find('task-state').text, 'Working');
+ assert.equal(c.find(reader).props.live, false, 'new text does not re-enable following');
+ assert.deepEqual(c.state.landing, landing);
+ assert.equal(c.action('history'), undefined);
+ const expected = events
+ .slice(1)
+ .map((event) => event.fact.text)
+ .join('');
+ assert.equal(c.state.live.map((fragment) => fragment.text).join(''), expected);
+ c.raw('task-latest').history(page(102, [chunk(1, 'Goal', { role: 'user' })]));
+ assert.equal(c.find(reader).props.live, true);
+ assert.ok(c.find(reader).props.messages.some((message) => message.text === expected));
+});
+
test('output review opens original structured evidence and full output at the same history fence', () => {
const tool = {
toolUseId: 'write-1',
@@ -414,3 +470,137 @@ test('long task visits stay within the UI call envelope while recent readers rem
assert.ok(c.state.visits.length <= 3);
assert.ok(Buffer.byteLength(JSON.stringify(c.out)) < 1024 * 1024);
});
+
+test('prefetching adjacent sections of one long message preserves the visible section and anchor', () => {
+ const part = (offset, text) => chunk(10, text, { offset, totalBytes: 60000 });
+ const cursor = (offset) => ({ sequence: 10, offset, field: 'body' });
+ const c = client()
+ .observe(20)
+ .history(page(20, [part(40000, 'Tail')], { before: cursor(40000) }));
+ const reader = c.reader();
+ const landing = structuredClone(c.state.landing);
+ const tail = c.find(reader).props.messages[0];
+ c.child(reader, 'older').history(
+ page(20, [part(20000, 'Middle')], {
+ before: cursor(20000),
+ after: cursor(40000),
+ }),
+ );
+ assert.deepEqual(c.state.landing, landing);
+ assert.deepEqual(c.find(reader).props.messages.at(-1), tail);
+ assert.equal(new Set(c.find(reader).props.messages.map((message) => message.id)).size, 2);
+ c.child(reader, 'older').history(page(20, [part(0, 'Start')], { after: cursor(20000) }));
+ assert.deepEqual(c.state.landing, landing);
+ assert.deepEqual(
+ c.find(reader).props.messages.map((message) => message.text),
+ ['Start', 'Middle', 'Tail'],
+ );
+ assert.equal(new Set(c.find(reader).props.messages.map((message) => message.id)).size, 3);
+});
+
+test('scrolling traverses empty history pages without another gesture or losing the visible anchor', () => {
+ const cursor = (sequence) => ({ sequence, offset: 0, field: 'body' });
+ for (const direction of ['older', 'newer']) {
+ const backward = direction === 'older';
+ const c = client()
+ .observe(100)
+ .history(
+ page(100, [chunk(50, 'Visible')], {
+ before: cursor(40),
+ after: cursor(60),
+ }),
+ );
+ const reader = c.reader();
+ const landing = structuredClone(c.state.landing);
+ c.child(reader, direction).history(
+ page(100, [], {
+ before: cursor(30),
+ after: cursor(70),
+ }),
+ );
+ assert.deepEqual(c.action('history').input.position, {
+ kind: backward ? 'before' : 'after',
+ cursor: cursor(backward ? 30 : 70),
+ });
+ assert.equal(c.action('history').input.through, 100);
+ assert.deepEqual(
+ c.find(reader).props.messages.map((message) => message.text),
+ ['Visible'],
+ );
+ c.history(
+ page(100, [chunk(backward ? 20 : 80, 'Adjacent')], {
+ before: backward ? null : cursor(70),
+ after: backward ? cursor(30) : null,
+ }),
+ );
+ assert.deepEqual(c.state.landing, landing);
+ assert.deepEqual(
+ c.find(reader).props.messages.map((message) => message.text),
+ backward ? ['Adjacent', 'Visible'] : ['Visible', 'Adjacent'],
+ );
+ }
+});
+
+test('a new turn cannot inherit the previous turn goal when its user message is outside the tail page', () => {
+ const c = client()
+ .observe(10)
+ .history(page(10, [chunk(1, 'Old goal', { role: 'user' })]));
+ assert.equal(c.find('task-goal').text, 'Old goal');
+ c.raw('task-latest').history(page(10, [chunk(9, 'New output', { turnId: 'turn-2' })]));
+ assert.equal(c.find('task-goal'), undefined);
+ c.raw('task-latest').history(
+ page(10, [chunk(8, 'New goal', { turnId: 'turn-2', role: 'user' })]),
+ );
+ assert.equal(c.find('task-goal').text, 'New goal');
+ c.raw('task-latest').history(page(10, [chunk(9, 'New output', { turnId: 'turn-2' })]));
+ assert.equal(c.find('task-goal').text, 'New goal');
+});
+
+test('reading older content on a short page suspends automatic following including an in-flight tail reply', () => {
+ const c = client()
+ .observe(10, { invocationId: 'run-a', turnId: 'turn-1', state: 'running' })
+ .history(
+ page(10, [chunk(9, 'Visible')], {
+ before: { sequence: 8, offset: 0, field: 'body' },
+ }),
+ );
+ const reader = c.reader();
+ c.state.latestKnown = 20;
+ c.child(reader, 'tailReady', { landing: c.state.landing.sequence, readingRevision: 1 });
+ const pending = c.action('history');
+ assert.ok(pending);
+ c.child(reader, 'older');
+ c.reply(pending, page(20, [chunk(19, 'New output')]));
+ assert.deepEqual(
+ c.find(reader).props.messages.map((message) => message.text),
+ ['Visible'],
+ );
+ c.child(reader, 'older').history(
+ page(10, [chunk(7, 'Older')], {
+ after: { sequence: 8, offset: 0, field: 'body' },
+ }),
+ );
+ c.child(reader, 'tailReady', { landing: c.state.landing.sequence, readingRevision: 2 });
+ assert.equal(c.action('history'), undefined);
+ assert.equal(c.find(reader).props.live, false);
+ assert.deepEqual(
+ c.find(reader).props.messages.map((message) => message.text),
+ ['Older', 'Visible'],
+ );
+ c.raw('task-latest').history(page(20, [chunk(19, 'New output')]));
+ assert.equal(c.find(reader).props.live, true);
+});
+
+test('a settled stop observation clears the stopping notice and shows the canonical stopped outcome', () => {
+ const c = client();
+ c.state.notice = 'stopping';
+ c.observe(10).history(
+ page(10, [
+ chunk(9, 'Partial output', {
+ timing: { startedAt: 1, finished: { at: 2, outcome: 'aborted' } },
+ }),
+ ]),
+ );
+ assert.equal(c.state.notice, null);
+ assert.equal(c.find('task-state').text, 'Stopped');
+});
diff --git a/products/maka/bundle/tests/timeline.test.mjs b/products/maka/bundle/tests/timeline.test.mjs
new file mode 100644
index 0000000000..a5eaabdaf8
--- /dev/null
+++ b/products/maka/bundle/tests/timeline.test.mjs
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+const { activate } = await import('../dist/packages/appearance/tui.js');
+const theme = activate({ scheme: 'dark' }).Appearance.value;
+const { component } = await import('../dist/packages/timeline/tui.js');
+
+test('history prefetches before an edge without treating stale or commanded viewports as scrolling', () => {
+ const props = {
+ theme,
+ locale: 'en',
+ copyRequest: null,
+ browse: null,
+ timings: [],
+ markReadEnabled: false,
+ toolContext: null,
+ waitingTools: [],
+ liveTools: [],
+ messages: Array.from({ length: 8 }, (_, i) => ({
+ id: String(i),
+ contentKey: String(i),
+ role: 'assistant',
+ text: 'Message ' + i,
+ timestamp: null,
+ source: i + 1,
+ image: false,
+ continued: false,
+ tool: null,
+ })),
+ source: null,
+ search: null,
+ loading: false,
+ older: true,
+ newer: true,
+ latest: false,
+ landing: { sequence: 0, edge: 'end' },
+ live: true,
+ };
+ let out = component.mount(props, {});
+ const observed = (revision, direction, visibleChildKeys, extra = {}) => {
+ out = component.event(
+ out.frame.state,
+ {
+ kind: 'component',
+ child: 'reading',
+ name: 'observed',
+ event: {
+ readingRevision: revision,
+ scroll: { readingRevision: revision, direction },
+ pendingCommand: null,
+ viewport: { readingRevision: revision, atStart: false, atEnd: false, visibleChildKeys },
+ ...extra,
+ },
+ },
+ props,
+ {},
+ );
+ return out.events.filter((event) => event.name === 'older' || event.name === 'newer');
+ };
+ assert.deepEqual(observed(1, 'up', ['message-3', 'message-4']), []);
+ assert.deepEqual(observed(2, 'up', ['message-1', 'message-2']), [{ name: 'older', value: {} }]);
+ assert.deepEqual(observed(2, 'up', ['message-1']), [], 'one request for each original scroll');
+ assert.deepEqual(observed(3, 'down', ['message-5', 'message-6']), [{ name: 'newer', value: {} }]);
+ assert.deepEqual(
+ observed(4, 'up', [], {
+ viewport: {
+ readingRevision: 3,
+ atStart: true,
+ atEnd: false,
+ visibleChildKeys: ['message-0'],
+ },
+ }),
+ [],
+ 'wait for the viewport actually produced by the scroll',
+ );
+ assert.deepEqual(observed(4, 'up', ['message-0'], { pendingCommand: 7 }), []);
+ assert.deepEqual(observed(4, 'up', ['message-0']), [{ name: 'older', value: {} }]);
+ props.messages = props.messages.slice(0, 2);
+ assert.deepEqual(
+ observed(5, 'up', ['message-1']),
+ [],
+ 'do not evict the visible tail of a two-part message',
+ );
+ assert.deepEqual(observed(6, 'up', ['message-0']), [{ name: 'older', value: {} }]);
+ assert.deepEqual(observed(7, 'down', ['message-0']), []);
+});
diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index 6e2a6be0bd..aa194fa09b 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -44,22 +44,22 @@
"plugins/agent/acp-executor/native/Cargo.toml": "6a896cb424d361ef3a98efa56a92ffc8680156473822bd1d38374b60f3f1da66",
"plugins/agent/acp-executor/package.ts": "9c055b7299b8d7eaf8c91bea7e684dd15164212ac2a6ef341322464bc88edfc5",
"plugins/agent/agent-screen/actions.ts": "93d59bc6ec3108a96d7840c5f9a25c132fd99610c98c7250f7657b9f94df2ecf",
- "plugins/agent/agent-screen/component-events.ts": "91a97fe4cdc654c9c218c1e0ad73b36a3b471fe88434b62d75d7078c5dd46960",
- "plugins/agent/agent-screen/conversation-view.ts": "6082c6bedd560ca4044546967f828a9c4e50460f360ea05f1502b81cf96c5914",
+ "plugins/agent/agent-screen/component-events.ts": "7e606d950294903e6b049b074422ac4538fdbf9421c0c36a7372dfec16716093",
+ "plugins/agent/agent-screen/conversation-view.ts": "24969c432934403acafd0c8e3351eb34c0372154534dfdcc247cb75d83e01e01",
"plugins/agent/agent-screen/desktop.ts": "d64f976e965c124ab736edf11adc5d998d22d021c8e85753f88a963d16f2834f",
"plugins/agent/agent-screen/events.ts": "5b3a966990d8434729b2d01270aef5c57e7bdf42d08ab43c82e723c2002e3e10",
- "plugins/agent/agent-screen/frame.ts": "c7952902574061f8918f9e9afd2312bdbfa4f315862fb71a5039a22d210edb36",
+ "plugins/agent/agent-screen/frame.ts": "4284b1d35dbfe70a36b751f77c02cb5bb084e3e0ef0d7896ee91e166e51cb32f",
"plugins/agent/agent-screen/header-view.ts": "8adf2d03c1cb523138ffc3ac97cb7c598ed76683af97e54091cf271a0c5c861c",
"plugins/agent/agent-screen/index.ts": "ef4ec3e1722887782d1d5f30b536ebb098abd026aa72e15067357ca3c23f98aa",
- "plugins/agent/agent-screen/messages.ts": "3a4ec6dc2600580ecd8310990ac95370591b810b152b3fdfad4b8c6be6e7ef80",
+ "plugins/agent/agent-screen/messages.ts": "f9bf92286bce263e023a8497c3aba97ec6f86e033c17c31140056639550dcd37",
"plugins/agent/agent-screen/navigation-view.ts": "5674c945ba8307387e295f8f398da61184dfcc662b1d10d9058b67c755a1096b",
"plugins/agent/agent-screen/outputs-view.ts": "85458c8556b9a5da0c36e8a29b0c1208b4b9a0a7948732c990e8830d52490395",
"plugins/agent/agent-screen/package.ts": "bc925efa98b53ff956ea7ec43b6d1b09e2a740b3b9a0525e4bef8e0877c44018",
"plugins/agent/agent-screen/pages-view.ts": "2102dea3cc5c739fed04f6e4f0d17d1d22ac01c25fae924a5e46baad972e95b3",
"plugins/agent/agent-screen/raw-events.ts": "b87b41494b30aa9847441496aa501798084df1d43500826f993bfd6e88b35182",
- "plugins/agent/agent-screen/results.ts": "b24de0e4467e98ecf7dac6c7b804374193780f943293f46831b12ebde278bb5b",
- "plugins/agent/agent-screen/state.ts": "f1ff2debdd571e7e80747549c05e18c3025bf04604094c932b935b4c8af04541",
- "plugins/agent/agent-screen/task-overview.ts": "3970ba9d4421eabe7d694d8ea0b373de59a3d8c4edfb9d275c51fcdcf0cc3f93",
+ "plugins/agent/agent-screen/results.ts": "1f1fca229db8a7a6ab35ba18564ddf2d1b5e86e7dfee5aa1df1f4457043b4c4b",
+ "plugins/agent/agent-screen/state.ts": "605a9ab488da78be5d4cc37ea1ec7d5c527cbff03a784a2f074c5a99e583b64d",
+ "plugins/agent/agent-screen/task-overview.ts": "4ceca90536fcc91619571d8d46caab33146ba6298f88bd85a626cc995c4acb27",
"plugins/agent/agent-screen/tui.ts": "c56f6d0f7e2382f67aef31a486b4524073a401d0ddc4dae4b92c7aaf9a7933db",
"plugins/agent/agent/package.ts": "b0124f84e1701973067f3d6d38b40f71cc5f6cd7b80a5c5d93744e957603abd9",
"plugins/agent/api/Cargo.toml": "876a95a3f126bcd1c0f590b84e307bebcf714117f7f1d28fe4a8eb7b524d703a",
@@ -257,7 +257,7 @@
"plugins/settings/model-default/package.ts": "a9be3d2e7d9fd7c7db382b0012adfa1fa53c2968b2aa40d5b92e86ca2cc0f198",
"plugins/settings/model-default/tui.ts": "a4601ba20b67e1d12c6738e094d290b8d0d40bfee83afca911e98ceeca9de92e",
"plugins/settings/models/desktop.ts": "bf15c189eae2714e63c244a3f33543969b4a424af27f06251f45b8fd2a91c72e",
- "plugins/settings/models/index.ts": "8bc904bd88103d92d0e76ae3489a5419e54f8537c1b5ad1d1b5aea2b7b2735cc",
+ "plugins/settings/models/index.ts": "8f1abf24c5f51102ca77e854f5eea8a07df703a9408c0ac35b20cbc599e9aef8",
"plugins/settings/models/package.ts": "f20f7f940e9f0d5f2ad6660166341782a595ca8dd4f2341b3dea6b07dc29f1fb",
"plugins/settings/models/tui.ts": "112e16f3166a7d3f2eaf11d557c7a18e94141229b183b1ea3d73986a413e1c1b",
"plugins/settings/native/Cargo.toml": "fe9e5c419efb8b518d99f2fcf3250300e496c01f157d20ca753c51e049a93a08",
@@ -271,9 +271,9 @@
"plugins/settings/store/Cargo.toml": "272ccd5e4f01936bb537bc96819be7ee13e12f00262b2a37a4844e482eedcf69",
"plugins/shared/agent-imports.ts": "14ce39353611cab4d5d4aa043737929986d147f474bf2b3f71dded265179870c",
"plugins/shared/appearance-editor.ts": "f564f53cd126f1ebe4fe8cca638823769dd6ff84512bf42f074053afa76ee4b5",
- "plugins/shared/attachments.ts": "9584769234c14281eb7cbdd9a816f4f47ca9b6fd20a6c58e4d5532b0183d93c9",
+ "plugins/shared/attachments.ts": "f44b8800041f136370adc93011fe49905e0e3e631893914f13c87d037523f5b0",
"plugins/shared/bookmark.ts": "d16b12b7aa9fd900271d6d8469c45d40e9295c8bd949adbcc7280f76e1d79dfc",
- "plugins/shared/components.ts": "a2ac18038b63e057b6087a6a2d2fe008a1e92d63216a956eca68169cc2cd3674",
+ "plugins/shared/components.ts": "d79d8538cd76762fc8e73fb5a83bb57d074b468db2604c855a66b2690de92cb6",
"plugins/shared/contracts.ts": "a5927f9d281e6057b2525fc5cb8e6f7f2073adfcaa8b25c26d4c60c7152c452a",
"plugins/shared/conversation-search.ts": "0f5c759e9d290cfdc82c7fbd5bcfe688e6b0004ba29b85a38ab30844de917a57",
"plugins/shared/deployment-document.ts": "0fd08cd83442f4300d8f4f6cadf18fada02f40bf46c523e93f99541a5319e0da",
@@ -291,7 +291,7 @@
"plugins/shared/settings.ts": "36f6d6e17af54ca538fe3e0d355679598477f0f4551398085e4476ba7b893395",
"plugins/shared/skill-import.ts": "cdca0bf2631cb25f5e420fbff799b10d3a0ed5f3ab0dd2fdcafa7e916fbe97f0",
"plugins/shared/terminal-theme.ts": "9022a58c837bf39292f04b6383eed3cd3e875bd9635570f871b065c87f5ba395",
- "plugins/shared/tool-cards.ts": "2fb0631f0ed68ab9b14e279a2adaae082fdc4b2cc59a8eea670eddc2bdbe0eef",
+ "plugins/shared/tool-cards.ts": "2eccd4e3445ca8f61c95dadb0d33adc549af1c217451421c5f6657db220c5426",
"plugins/shared/tui-locale.ts": "4493d55c4e7e678f3187b552843d2295736ca16225a6fda19b82ab379090c525",
"plugins/shared/value.ts": "9b1b52b05ca45a4e37fbab6454810fca2a474c8532941e25c15ab897aaa2d607",
"plugins/shared/view.ts": "c4cf9fa7368adaf8d2da9541efcfc83b3328363c1ce617976af416e07fb316cc",
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org