@@ -2,11 +2,11 @@ package com.neatstudio.tmuxandroid;
import android.Manifest ;
import android.app.Activity ;
import android.app.AlertDialog ;
import android.app.Dialog ;
import android.content.ClipData ;
import android.content.ClipboardManager ;
import android.content.Context ;
import android.content.DialogInterface ;
import android.content.Intent ;
import android.content.SharedPreferences ;
import android.content.pm.PackageManager ;
@@ -27,6 +27,7 @@ import android.text.TextUtils;
import android.text.InputType ;
import android.text.method.LinkMovementMethod ;
import android.view.Gravity ;
import android.view.DragEvent ;
import android.view.HapticFeedbackConstants ;
import android.view.KeyEvent ;
import android.view.MotionEvent ;
@@ -140,6 +141,8 @@ public final class MainActivity extends Activity {
private TextView terminalMetaText ;
private TextView terminalConnectionText ;
private ScrollView terminalScroll ;
private LinearLayout agentActivityList ;
private Dialog agentActivityDialog ;
private LinearLayout terminalChatList ;
private TextView terminalLiveStatusText ;
private TextView terminalLiveOutputText ;
@@ -167,6 +170,7 @@ public final class MainActivity extends Activity {
private TerminalScreenBuffer terminalScreen = new TerminalScreenBuffer ( DEFAULT_TERMINAL_COLS , DEFAULT_TERMINAL_ROWS ) ;
private final StringBuilder queuedTerminalInput = new StringBuilder ( ) ;
private final List < ConversationMessage > terminalConversationMessages = new ArrayList < > ( ) ;
private final Set < String > expandedAgentActivity = new HashSet < > ( ) ;
private boolean terminalConnected ;
private boolean terminalConnecting ;
private boolean terminalRenderPending ;
@@ -290,17 +294,11 @@ public final class MainActivity extends Activity {
ScrollView scroll = new ScrollView ( this ) ;
LinearLayout content = pageContent ( ) ;
List < String > serverUrls = savedServerUrls ( ) ;
content . addView ( createPageHeader (
serverUrls . size ( ) + " nodes " ,
" Servers " ,
" + Add" ,
view - > promptCustomServer ( )
) ) ;
content . addView ( createServerPageHeader ( serverUrls . size ( ) + " nodes " ) ) ;
for ( String url : serverUrls ) {
content . addView ( serverProfileCard ( url ) , spacedHeight ( 72 , 0 , 12 ) ) ;
}
content . addView ( createServerUtilityRow ( ) ) ;
scroll . addView ( content ) ;
root . addView ( scroll , new LinearLayout . LayoutParams (
ViewGroup . LayoutParams . MATCH_PARENT ,
@@ -520,14 +518,46 @@ public final class MainActivity extends Activity {
return header ;
}
private View createServerUtilityRow ( ) {
LinearLayout row = new LinearLayout ( this ) ;
row . setOrientation ( LinearLayout . HORIZONTAL ) ;
row . setPadding ( 0 , dp ( 4 ) , 0 , 0 ) ;
row . addView ( compactButton ( " Probe all " , view - > probeServerProfiles ( ) ) ) ;
row . addView ( compactButton ( " Update " , view - > renderUpdateScreen ( ) ) ) ;
row . addView ( compactButton ( " More " , view - > showMainNavigation ( ) ) ) ;
return row ;
private View createServerPageHeader ( String eyebrow ) {
LinearLayout header = new LinearLayout ( this ) ;
header . setOrientation ( LinearLayout . HORIZONTAL ) ;
header . setGravity ( Gravity . BOTTOM ) ;
header . setPadding ( dp ( 2 ) , dp ( 6 ) , dp ( 2 ) , dp ( 18 ) ) ;
LinearLayout titleBlock = new LinearLayout ( this ) ;
titleBlock . setOrientation ( LinearLayout . VERTICAL ) ;
TextView eyebrowView = new TextView ( this ) ;
eyebrowView . setText ( eyebrow . toUpperCase ( java . util . Locale . ROOT ) ) ;
eyebrowView . setTextColor ( COLOR_TEXT_MUTED ) ;
eyebrowView . setTextSize ( 10 ) ;
eyebrowView . setTypeface ( Typeface . MONOSPACE ) ;
TextView title = new TextView ( this ) ;
title . setText ( " Servers " ) ;
title . setTextColor ( COLOR_TEXT ) ;
title . setTextSize ( 25 ) ;
title . setTypeface ( Typeface . DEFAULT_BOLD ) ;
title . setIncludeFontPadding ( false ) ;
titleBlock . addView ( eyebrowView ) ;
titleBlock . addView ( title ) ;
header . addView ( titleBlock , new LinearLayout . LayoutParams ( 0 , ViewGroup . LayoutParams . WRAP_CONTENT , 1 ) ) ;
addHeaderTool ( header , " ↻ " , " Refresh servers " , view - > probeServerProfiles ( ) ) ;
addHeaderTool ( header , " + " , " Add server " , view - > promptCustomServer ( ) ) ;
addHeaderTool ( header , " ⋮ " , " More navigation " , view - > showMainNavigation ( ) ) ;
return header ;
}
private void addHeaderTool (
LinearLayout header ,
String label ,
String description ,
View . OnClickListener action
) {
Button button = terminalToolButton ( label , action ) ;
button . setContentDescription ( description ) ;
LinearLayout . LayoutParams params = new LinearLayout . LayoutParams ( dp ( 38 ) , dp ( 38 ) ) ;
params . leftMargin = dp ( 4 ) ;
header . addView ( button , params ) ;
}
private View createSessionPageHeader ( ) {
@@ -566,25 +596,11 @@ public final class MainActivity extends Activity {
titleBlock . addView ( title ) ;
row . addView ( titleBlock , new LinearLayout . LayoutParams ( 0 , ViewGroup . LayoutParams . WRAP_CONTENT , 1 ) ) ;
row . addView ( primaryButton ( " + Session" , view - > promptCreateSession ( ) ) , new LinearLayout . LayoutParams (
ViewGroup . LayoutParams . WRAP_CONTENT ,
dp ( 38 )
) ) ;
addHeaderTool ( row , " ⚙ " , " Session display settings " , view - > promptSessionSettings ( " " ) ) ;
addHeaderTool ( row , " ↻ " , " Refresh sessions " , view - > refreshSessions ( ) ) ;
addHeaderTool ( row , " + " , " Create session " , view - > promptCreateSession ( ) ) ;
addHeaderTool ( row , " ▰ " , " Manage groups " , view - > showGroupManagement ( ) ) ;
header . addView ( row , matchWrap ( ) ) ;
LinearLayout utilities = new LinearLayout ( this ) ;
utilities . setOrientation ( LinearLayout . HORIZONTAL ) ;
utilities . setGravity ( Gravity . END | Gravity . CENTER_VERTICAL ) ;
Button display = terminalToolButton ( " ⚙ " , view - > promptSessionSettings ( " " ) ) ;
display . setContentDescription ( " Session display settings " ) ;
utilities . addView ( display ) ;
Button refresh = compactButton ( " ↻ Refresh " , view - > refreshSessions ( ) ) ;
refresh . setContentDescription ( " Refresh sessions " ) ;
utilities . addView ( refresh ) ;
Button addGroup = compactButton ( " + New group" , view - > promptCreateKanbanProject ( ) ) ;
addGroup . setContentDescription ( " Add group " ) ;
utilities . addView ( addGroup ) ;
header . addView ( utilities , spacedMatchWrap ( 8 , 0 ) ) ;
return header ;
}
@@ -725,7 +741,7 @@ public final class MainActivity extends Activity {
}
private void confirmRemoveSavedServer ( String url ) {
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Remove server " )
. setMessage ( " Remove " + url + " from this device? " )
. setNegativeButton ( " Cancel " , null )
@@ -759,12 +775,12 @@ public final class MainActivity extends Activity {
}
private void showMainNavigation ( ) {
String [ ] items = { " Projects " , " Tools " , " Update " , " About " } ;
new AlertDialog . Builder ( this )
String [ ] items = { " Sessions / groups " , " Tools " , " Update " , " About " } ;
new AppDialogBuilder ( )
. setTitle ( " tmuxctl " )
. setItems ( items , ( dialog , which ) - > {
if ( which = = 0 ) {
renderProjectsScreen ( ) ;
openSessionPage ( ) ;
} else if ( which = = 1 ) {
renderToolsScreen ( ) ;
} else if ( which = = 2 ) {
@@ -810,7 +826,7 @@ public final class MainActivity extends Activity {
) ) ;
content . addView ( sectionTitle ( " Kanban / Messages " ) ) ;
content . addView ( actionPanel (
actionButton ( " Projects " , view - > showRaw ( " Kanban projects " , ( ) - > api . kanbanProjects ( ) ) ) ,
actionButton ( " Groups " , view - > showRaw ( " Groups " , ( ) - > api . kanbanProjects ( ) ) ) ,
actionButton ( " New project " , view - > promptCreateKanbanProject ( ) ) ,
actionButton ( " Delete project " , view - > promptDeleteKanbanProject ( ) ) ,
actionButton ( " Remove session " , view - > promptRemoveKanbanSession ( ) )
@@ -1024,11 +1040,15 @@ public final class MainActivity extends Activity {
LinearLayout actions = new LinearLayout ( this ) ;
actions . setOrientation ( LinearLayout . HORIZONTAL ) ;
actions . setGravity ( Gravity . END | Gravity . CENTER_VERTICAL ) ;
Button copy = compactButton ( " Copy commands " , view - > copyText ( " Server install " , SERVER_INSTALL_COMMANDS ) ) ;
actions . addView ( copy , new LinearLayout . LayoutParams ( 0 , dp ( 40 ) , 1 ) ) ;
actions . addView ( copy , new LinearLayout . LayoutParams ( ViewGroup . LayoutParams . WRAP_CONTENT , dp ( 32 ) ) ) ;
Button add = compactButton ( " Add server " , view - > promptCustomServer ( ) ) ;
LinearLayout . LayoutParams addParams = new LinearLayout . LayoutParams ( 0 , dp ( 40 ) , 1 ) ;
addParams . leftMargin = dp ( 7 ) ;
LinearLayout . LayoutParams addParams = new LinearLayout . LayoutParams (
ViewGroup . LayoutParams . WRAP_CONTENT ,
dp ( 32 )
) ;
addParams . leftMargin = dp ( 5 ) ;
actions . addView ( add , addParams ) ;
panel . addView ( actions , spacedMatchWrap ( 10 , 0 ) ) ;
@@ -1212,7 +1232,6 @@ public final class MainActivity extends Activity {
row . setPadding ( dp ( 8 ) , dp ( 5 ) , dp ( 8 ) , dp ( 5 ) ) ;
row . addView ( navButton ( PAGE_SERVERS , selected , view - > renderServerScreen ( ) ) ) ;
row . addView ( navButton ( PAGE_SESSIONS , selected , view - > openSessionPage ( ) ) ) ;
row . addView ( navButton ( PAGE_PROJECTS , selected , view - > renderProjectsScreen ( ) ) ) ;
row . addView ( navButton ( PAGE_TOOLS , selected , view - > renderToolsScreen ( ) ) ) ;
row . addView ( navButton ( PAGE_UPDATE , selected , view - > renderUpdateScreen ( ) ) ) ;
row . addView ( navButton ( PAGE_ABOUT , selected , view - > renderAboutScreen ( ) ) ) ;
@@ -1291,28 +1310,40 @@ public final class MainActivity extends Activity {
}
private LinearLayout actionPanel ( Button . . . buttons ) {
final int columns = 3 ;
LinearLayout panel = new LinearLayout ( this ) ;
panel . setOrientation ( LinearLayout . VERTICAL ) ;
panel . setPadding ( dp ( 6 ) , dp ( 6 ) , dp ( 6 ) , dp ( 1 ) ) ;
panel . setPadding ( dp ( 4 ) , dp ( 4 ) , dp ( 4 ) , 0 ) ;
panel . setBackground ( panelBackground ( ) ) ;
LinearLayout row = null ;
for ( int index = 0 ; index < buttons . length ; index + + ) {
if ( index % 2 = = 0 ) {
if ( index % columns = = 0 ) {
row = new LinearLayout ( this ) ;
row . setOrientation ( LinearLayout . HORIZONTAL ) ;
panel . addView ( row , matchWrap ( ) ) ;
}
LinearLayout . LayoutParams params = new LinearLayout . LayoutParams ( 0 , dp ( 38 ) , 1 ) ;
params . leftMargin = dp ( 3 ) ;
params . rightMargin = dp ( 3 ) ;
params . bottomMargin = dp ( 6 ) ;
LinearLayout . LayoutParams params = new LinearLayout . LayoutParams ( 0 , dp ( 32 ) , 1 ) ;
params . leftMargin = dp ( 2 ) ;
params . rightMargin = dp ( 2 ) ;
params . bottomMargin = dp ( 4 ) ;
buttons [ index ] . setTextSize ( 9 ) ;
buttons [ index ] . setPadding ( dp ( 4 ) , 0 , dp ( 4 ) , 0 ) ;
row . addView ( buttons [ index ] , params ) ;
}
int remainder = buttons . length % columns ;
if ( row ! = null & & remainder ! = 0 ) {
for ( int index = remainder ; index < columns ; index + + ) {
LinearLayout . LayoutParams spacerParams = new LinearLayout . LayoutParams ( 0 , dp ( 32 ) , 1 ) ;
spacerParams . leftMargin = dp ( 2 ) ;
spacerParams . rightMargin = dp ( 2 ) ;
row . addView ( new View ( this ) , spacerParams ) ;
}
}
LinearLayout . LayoutParams params = new LinearLayout . LayoutParams (
ViewGroup . LayoutParams . MATCH_PARENT ,
ViewGroup . LayoutParams . WRAP_CONTENT
) ;
params . bottomMargin = dp ( 8 ) ;
params . bottomMargin = dp ( 6 ) ;
panel . setLayoutParams ( params ) ;
return panel ;
}
@@ -1453,6 +1484,7 @@ public final class MainActivity extends Activity {
private View sessionGroupSection ( String name , List < SessionSummary > sessions , boolean project ) {
LinearLayout section = new LinearLayout ( this ) ;
section . setOrientation ( LinearLayout . VERTICAL ) ;
section . setOnDragListener ( ( view , event ) - > handleSessionDrop ( section , name , project , event ) ) ;
LinearLayout heading = new LinearLayout ( this ) ;
heading . setOrientation ( LinearLayout . HORIZONTAL ) ;
@@ -1475,6 +1507,10 @@ public final class MainActivity extends Activity {
count . setTextSize ( 11 ) ;
count . setTypeface ( Typeface . MONOSPACE ) ;
heading . addView ( count ) ;
Button actions = terminalToolButton ( " ⋮ " , view - > showGroupActions ( name , project ) ) ;
actions . setContentDescription ( " Actions for " + name ) ;
heading . addView ( new View ( this ) , new LinearLayout . LayoutParams ( 0 , 1 , 1 ) ) ;
heading . addView ( actions ) ;
section . addView ( heading ) ;
if ( sessions . isEmpty ( ) ) {
@@ -1486,13 +1522,79 @@ public final class MainActivity extends Activity {
section . addView ( empty , matchWrap ( ) ) ;
} else {
for ( SessionSummary session : sessions ) {
section . addView ( sessionRow ( session ) , spacedMatchWrap ( 0 , 10 ) ) ;
section . addView ( sessionRow ( session , project ? name : null ) , spacedMatchWrap ( 0 , 10 ) ) ;
}
}
return section ;
}
private boolean handleSessionDrop (
LinearLayout section ,
String targetGroup ,
boolean targetIsGroup ,
DragEvent event
) {
if ( ! ( event . getLocalState ( ) instanceof SessionDrag ) ) {
return false ;
}
if ( event . getAction ( ) = = DragEvent . ACTION_DRAG_ENTERED ) {
section . setBackground ( rounded ( COLOR_CARD_ALT , 8 , COLOR_ACCENT , 1 ) ) ;
return true ;
}
if ( event . getAction ( ) = = DragEvent . ACTION_DRAG_EXITED
| | event . getAction ( ) = = DragEvent . ACTION_DRAG_ENDED ) {
section . setBackgroundColor ( Color . TRANSPARENT ) ;
return true ;
}
if ( event . getAction ( ) = = DragEvent . ACTION_DROP ) {
section . setBackgroundColor ( Color . TRANSPARENT ) ;
SessionDrag drag = ( SessionDrag ) event . getLocalState ( ) ;
moveSessionBetweenGroups ( drag . sessionName , drag . sourceGroup , targetIsGroup ? targetGroup : null ) ;
return true ;
}
return true ;
}
private void showGroupActions ( String groupName , boolean project ) {
String [ ] items = project
? new String [ ] { " Add session " , " Messages " , " Delete group " }
: new String [ ] { " Create group " , " Refresh sessions " } ;
new AppDialogBuilder ( )
. setTitle ( groupName )
. setItems ( items , ( dialog , which ) - > {
if ( ! project ) {
if ( which = = 0 ) {
promptCreateKanbanProject ( ) ;
} else {
refreshSessions ( ) ;
}
} else if ( which = = 0 ) {
promptAddKanbanSessionToProject ( groupName ) ;
} else if ( which = = 1 ) {
showRaw ( " Group messages: " + groupName , ( ) - > api . groupMessages ( groupName ) ) ;
} else {
runApiAction ( " Delete group " , ( ) - > api . deleteKanbanProject ( groupName ) ) ;
}
} )
. show ( ) ;
}
private void showGroupManagement ( ) {
new AppDialogBuilder ( )
. setTitle ( " Groups " )
. setItems ( new String [ ] { " Create group " , " Delete group " , " Group messages " } , ( dialog , which ) - > {
if ( which = = 0 ) {
promptCreateKanbanProject ( ) ;
} else if ( which = = 1 ) {
promptDeleteKanbanProject ( ) ;
} else {
promptGroupMessages ( ) ;
}
} )
. show ( ) ;
}
private void refreshProjects ( ) {
if ( projectList ! = null ) {
projectList . removeAllViews ( ) ;
@@ -1640,7 +1742,7 @@ public final class MainActivity extends Activity {
list . addView ( empty ) ;
}
for ( SessionSummary session : sessions ) {
list . addView ( sessionRow ( session ) , spacedMatchWrap ( 0 , 10 ) ) ;
list . addView ( sessionRow ( session , null ) , spacedMatchWrap ( 0 , 10 ) ) ;
}
if ( sessionSummaryText ! = null ) {
sessionSummaryText . setText ( " Server: " + getServerUrl ( ) + " \ nSessions: " + sessions . size ( ) ) ;
@@ -1648,7 +1750,7 @@ public final class MainActivity extends Activity {
setStatus ( " Loaded " + sessions . size ( ) + " sessions " ) ;
}
private View sessionRow ( SessionSummary session ) {
private View sessionRow ( SessionSummary session , String sourceGroup ) {
LinearLayout row = new LinearLayout ( this ) ;
row . setOrientation ( LinearLayout . HORIZONTAL ) ;
row . setGravity ( Gravity . CENTER_VERTICAL ) ;
@@ -1658,7 +1760,13 @@ public final class MainActivity extends Activity {
row . setOnClickListener ( view - > openTerminal ( session . name ) ) ;
row . setOnLongClickListener ( view - > {
view . performHapticFeedback ( HapticFeedbackConstants . LONG_PRESS ) ;
showSessionActions ( session . name ) ;
ClipData data = ClipData . newPlainText ( " tmux-session " , session . name ) ;
view . startDragAndDrop (
data ,
new View . DragShadowBuilder ( view ) ,
new SessionDrag ( session . name , sourceGroup ) ,
0
) ;
return true ;
} ) ;
@@ -1712,7 +1820,7 @@ public final class MainActivity extends Activity {
row . addView ( textBlock , new LinearLayout . LayoutParams ( 0 , ViewGroup . LayoutParams . WRAP_CONTENT , 1 ) ) ;
Button move = terminalToolButton ( " ⇄ " , view - > promptAddKanbanSession ( session . name ) ) ;
move . setContentDescription ( " Move " + session . name + " to p roject " ) ;
move . setContentDescription ( " Move " + session . name + " to g roup " ) ;
row . addView ( move ) ;
Button kill = terminalToolButton ( " × " , view - > confirmKill ( session . name ) ) ;
kill . setContentDescription ( " Kill session " ) ;
@@ -1826,7 +1934,7 @@ public final class MainActivity extends Activity {
}
private void confirmKill ( String sessionName ) {
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Kill session " )
. setMessage ( " Kill tmux session \" " + sessionName + " \" ? " )
. setNegativeButton ( " Cancel " , null )
@@ -1845,6 +1953,9 @@ public final class MainActivity extends Activity {
}
private void openTerminal ( String sessionName ) {
if ( agentActivityDialog ! = null ) {
agentActivityDialog . dismiss ( ) ;
}
closeTerminalSocket ( ) ;
activeSessionName = sessionName ;
loadTerminalDisplaySettings ( sessionName ) ;
@@ -1858,6 +1969,10 @@ public final class MainActivity extends Activity {
terminalFollowOutput = true ;
terminalKeyPage = 0 ;
terminalHistoryOffset = 0 ;
terminalConversationMessages . clear ( ) ;
expandedAgentActivity . clear ( ) ;
agentActivityList = null ;
agentActivityDialog = null ;
terminalImagePath = " " ;
terminalImagePreviewBar = null ;
terminalImagePreview = null ;
@@ -2096,6 +2211,200 @@ public final class MainActivity extends Activity {
} ) ;
}
private void showAgentActivity ( ) {
String sessionName = activeSessionName ;
if ( sessionName = = null | | sessionName . isEmpty ( ) ) {
showMessage ( " Open a terminal before viewing agent activity " ) ;
return ;
}
if ( agentActivityDialog ! = null & & agentActivityDialog . isShowing ( ) ) {
return ;
}
Dialog dialog = new Dialog ( this ) ;
LinearLayout sheet = bottomSheetContent ( dialog , " Agent activity " , compactLabel ( sessionName , 42 ) ) ;
Button refresh = compactButton ( " Refresh " , view - > refreshAgentActivity ( sessionName ) ) ;
refresh . setContentDescription ( " Refresh agent activity " ) ;
sheet . addView ( refresh , spacedMatchWrap ( 0 , 10 ) ) ;
agentActivityList = new LinearLayout ( this ) ;
agentActivityList . setOrientation ( LinearLayout . VERTICAL ) ;
agentActivityList . addView ( projectStateText ( " Loading agent activity... " ) , matchWrap ( ) ) ;
sheet . addView ( agentActivityList , matchWrap ( ) ) ;
agentActivityDialog = dialog ;
dialog . setOnDismissListener ( ignored - > {
if ( agentActivityDialog = = dialog ) {
agentActivityDialog = null ;
agentActivityList = null ;
}
} ) ;
showBottomSheet ( dialog , sheet , null ) ;
refreshAgentActivity ( sessionName ) ;
}
private void refreshAgentActivity ( String sessionName ) {
executor . execute ( ( ) - > {
try {
JSONObject rootObject = new JSONObject ( api . timeline ( 200 ) ) ;
JSONArray events = rootObject . optJSONArray ( " events " ) ;
List < ConversationMessage > loaded = new ArrayList < > ( ) ;
for ( int index = 0 ; events ! = null & & index < events . length ( ) ; index + + ) {
JSONObject event = events . optJSONObject ( index ) ;
if ( event ! = null
& & " conversation-message " . equals ( event . optString ( " type " ) )
& & sessionName . equals ( event . optString ( " sessionName " ) ) ) {
loaded . add ( ConversationMessage . fromJson ( event ) ) ;
}
}
Collections . sort ( loaded , ( left , right ) - > left . createdAt . compareTo ( right . createdAt ) ) ;
runOnUiThread ( ( ) - > {
if ( ! sessionName . equals ( activeSessionName ) ) {
return ;
}
terminalConversationMessages . clear ( ) ;
terminalConversationMessages . addAll ( loaded ) ;
renderAgentActivity ( ) ;
} ) ;
} catch ( Exception error ) {
runOnUiThread ( ( ) - > {
if ( sessionName . equals ( activeSessionName ) & & agentActivityList ! = null ) {
agentActivityList . removeAllViews ( ) ;
agentActivityList . addView ( projectStateText ( " Activity unavailable \ n " + error . getMessage ( ) ) , matchWrap ( ) ) ;
}
} ) ;
}
} ) ;
}
private void upsertAgentActivity ( ConversationMessage message ) {
if ( message . sessionName . isEmpty ( ) | | ! message . sessionName . equals ( activeSessionName ) ) {
return ;
}
if ( ! message . local & & " user " . equals ( message . role ) ) {
for ( int index = terminalConversationMessages . size ( ) - 1 ; index > = 0 ; index - - ) {
ConversationMessage existing = terminalConversationMessages . get ( index ) ;
if ( existing . local & & existing . content . equals ( message . content ) ) {
terminalConversationMessages . remove ( index ) ;
break ;
}
}
}
for ( int index = 0 ; index < terminalConversationMessages . size ( ) ; index + + ) {
if ( terminalConversationMessages . get ( index ) . messageId . equals ( message . messageId ) ) {
terminalConversationMessages . set ( index , message ) ;
renderAgentActivity ( ) ;
return ;
}
}
terminalConversationMessages . add ( message ) ;
Collections . sort ( terminalConversationMessages ,
( left , right ) - > left . createdAt . compareTo ( right . createdAt ) ) ;
renderAgentActivity ( ) ;
}
private void renderAgentActivity ( ) {
if ( agentActivityList = = null ) {
return ;
}
agentActivityList . removeAllViews ( ) ;
if ( terminalConversationMessages . isEmpty ( ) ) {
TextView empty = bodyText ( " No structured agent activity yet " ) ;
empty . setPadding ( dp ( 8 ) , dp ( 18 ) , dp ( 8 ) , dp ( 18 ) ) ;
agentActivityList . addView ( empty , matchWrap ( ) ) ;
return ;
}
for ( ConversationMessage message : terminalConversationMessages ) {
agentActivityList . addView ( isAgentProcessNoise ( message )
? agentProcessRow ( message ) : agentMessageRow ( message ) , spacedMatchWrap ( 0 , 6 ) ) ;
}
}
private boolean isAgentProcessNoise ( ConversationMessage message ) {
if ( " tool " . equals ( message . role )
| | " tool " . equals ( message . contentType )
| | " command " . equals ( message . contentType ) ) {
return true ;
}
String text = message . content . trim ( )
. replaceFirst ( " ^[•●] \\ s* " , " " )
. toLowerCase ( java . util . Locale . ROOT ) ;
return text . startsWith ( " ran " )
| | text . startsWith ( " explored " )
| | text . startsWith ( " viewed image " )
| | text . startsWith ( " waiting for agent " )
| | text . startsWith ( " waiting for agents " )
| | text . startsWith ( " searched " )
| | text . startsWith ( " read " )
| | text . startsWith ( " listed " )
| | text . startsWith ( " downloaded " ) ;
}
private View agentProcessRow ( ConversationMessage message ) {
LinearLayout row = new LinearLayout ( this ) ;
row . setOrientation ( LinearLayout . VERTICAL ) ;
row . setPadding ( dp ( 10 ) , dp ( 7 ) , dp ( 10 ) , dp ( 7 ) ) ;
row . setBackground ( rounded ( COLOR_CARD_ALT , 6 , COLOR_BORDER_SOFT , 1 ) ) ;
String key = message . messageId . isEmpty ( ) ? message . createdAt + " : " + message . content : message . messageId ;
boolean expanded = expandedAgentActivity . contains ( key ) ;
TextView summary = new TextView ( this ) ;
summary . setText ( ( expanded ? " ▼ " : " ▶ " ) + agentProcessLabel ( message ) ) ;
summary . setTextColor ( COLOR_ACCENT_WARM ) ;
summary . setTextSize ( 10 ) ;
summary . setTypeface ( Typeface . MONOSPACE , Typeface . BOLD ) ;
summary . setPadding ( 0 , 0 , 0 , expanded ? dp ( 6 ) : 0 ) ;
summary . setOnClickListener ( view - > {
if ( ! expandedAgentActivity . add ( key ) ) {
expandedAgentActivity . remove ( key ) ;
}
renderAgentActivity ( ) ;
} ) ;
summary . setContentDescription ( ( expanded ? " Collapse " : " Expand " ) + agentProcessLabel ( message ) ) ;
row . addView ( summary , matchWrap ( ) ) ;
if ( expanded ) {
TextView detail = agentActivityContent ( message ) ;
detail . setPadding ( dp ( 8 ) , dp ( 4 ) , 0 , 0 ) ;
row . addView ( detail , matchWrap ( ) ) ;
}
return row ;
}
private View agentMessageRow ( ConversationMessage message ) {
LinearLayout row = new LinearLayout ( this ) ;
row . setOrientation ( LinearLayout . VERTICAL ) ;
row . setPadding ( dp ( 10 ) , dp ( 7 ) , dp ( 10 ) , dp ( 7 ) ) ;
boolean user = " user " . equals ( message . role ) ;
row . setBackground ( rounded ( user ? COLOR_ACCENT_DARK : COLOR_CARD , 6 ,
user ? COLOR_ACCENT_DARK : COLOR_BORDER_SOFT , 1 ) ) ;
TextView label = new TextView ( this ) ;
label . setText ( user ? " YOU " : " AGENT " ) ;
label . setTextColor ( user ? COLOR_ACCENT : COLOR_SUCCESS ) ;
label . setTextSize ( 9 ) ;
label . setTypeface ( Typeface . MONOSPACE , Typeface . BOLD ) ;
row . addView ( label , matchWrap ( ) ) ;
row . addView ( agentActivityContent ( message ) , matchWrap ( ) ) ;
return row ;
}
private TextView agentActivityContent ( ConversationMessage message ) {
TextView content = new TextView ( this ) ;
content . setText ( message . content . isEmpty ( ) ? " (empty) " : message . content ) ;
content . setTextColor ( COLOR_TEXT ) ;
content . setTextSize ( terminalFontSizeSp ) ;
content . setTypeface ( terminalTypeface ( ) ) ;
content . setLineSpacing ( 0 , terminalLineHeight ) ;
content . setLetterSpacing ( terminalLetterSpacing ) ;
content . setTextIsSelectable ( true ) ;
content . setPadding ( 0 , dp ( 5 ) , 0 , 0 ) ;
return content ;
}
private String agentProcessLabel ( ConversationMessage message ) {
String text = message . content . replaceAll ( " \\ s+ " , " " ) . trim ( ) ;
if ( ! message . toolName . isEmpty ( ) ) {
text = message . toolName + ( text . isEmpty ( ) ? " " : " · " + text ) ;
}
return compactLabel ( defaultValue ( text , " Agent activity " ) , 68 ) ;
}
private void refreshTerminalConversation ( String sessionName ) {
executor . execute ( ( ) - > {
try {
@@ -2537,6 +2846,7 @@ public final class MainActivity extends Activity {
new SheetAction ( " ↻ " , " Reconnect " , ( ) - > connectTerminal ( sessionName ) ) ,
new SheetAction ( " ◎ " , " Status " , ( ) - > showRaw ( " Session status " , ( ) - > api . sessionStatus ( sessionName ) ) ) ,
new SheetAction ( " Aa " , " Display " , ( ) - > promptSessionSettings ( " " ) ) ,
new SheetAction ( " ≡ " , " Activity " , this : : showAgentActivity ) ,
new SheetAction ( " ↑ " , " Older " , ( ) - > scrollTerminalHistory ( - terminalRows ) ) ,
new SheetAction ( " ↓ " , " Newer " , ( ) - > scrollTerminalHistory ( terminalRows ) )
) ;
@@ -2808,7 +3118,103 @@ public final class MainActivity extends Activity {
}
private void promptAddKanbanSession ( String sessionName ) {
promptText ( " Add to kanban project " , " project " , " " , projectName - > api . addKanbanSession ( projectName , sessionName ) ) ;
progressBar . setVisibility ( View . VISIBLE ) ;
executor . execute ( ( ) - > {
try {
List < String > groups = kanbanGroupNames ( api . kanbanProjects ( ) ) ;
runOnUiThread ( ( ) - > {
progressBar . setVisibility ( View . GONE ) ;
List < String > choices = new ArrayList < > ( ) ;
choices . add ( " Ungrouped " ) ;
choices . addAll ( groups ) ;
choices . add ( " + Create new group" ) ;
new AppDialogBuilder ( )
. setTitle ( " Move " + sessionName )
. setItems ( choices . toArray ( new String [ 0 ] ) , ( dialog , which ) - > {
if ( which = = 0 ) {
moveSessionToGroup ( sessionName , null ) ;
} else if ( which = = choices . size ( ) - 1 ) {
promptCreateGroupForSession ( sessionName ) ;
} else {
moveSessionToGroup ( sessionName , choices . get ( which ) ) ;
}
} )
. show ( ) ;
} ) ;
} catch ( Exception error ) {
runOnUiThread ( ( ) - > {
progressBar . setVisibility ( View . GONE ) ;
showMessage ( " Group load failed: " + error . getMessage ( ) ) ;
} ) ;
}
} ) ;
}
private List < String > kanbanGroupNames ( String projectsText ) throws Exception {
List < String > groups = new ArrayList < > ( ) ;
JSONObject object = new JSONObject ( projectsText = = null | | projectsText . isEmpty ( ) ? " {} " : projectsText ) ;
JSONArray projects = object . optJSONArray ( " projects " ) ;
for ( int index = 0 ; projects ! = null & & index < projects . length ( ) ; index + + ) {
JSONObject project = projects . optJSONObject ( index ) ;
if ( project ! = null & & ! project . optString ( " name " ) . isEmpty ( ) ) {
groups . add ( project . optString ( " name " ) ) ;
}
}
return groups ;
}
private void promptCreateGroupForSession ( String sessionName ) {
promptText ( " Create group for " + sessionName , " group name " , " " , groupName - > {
if ( groupName . isEmpty ( ) ) {
throw new IllegalArgumentException ( " Group name is required " ) ;
}
api . createKanbanProject ( groupName , " ~ " , " " ) ;
api . addKanbanSession ( groupName , sessionName ) ;
} ) ;
}
private void moveSessionToGroup ( String sessionName , String targetGroup ) {
runApiAction ( " Move session " , ( ) - > {
JSONObject object = new JSONObject ( api . kanbanProjects ( ) ) ;
JSONArray projects = object . optJSONArray ( " projects " ) ;
boolean alreadyInTarget = false ;
for ( int projectIndex = 0 ; projects ! = null & & projectIndex < projects . length ( ) ; projectIndex + + ) {
JSONObject project = projects . optJSONObject ( projectIndex ) ;
if ( project = = null ) {
continue ;
}
String projectName = project . optString ( " name " ) ;
JSONArray agents = project . optJSONArray ( " agents " ) ;
for ( int agentIndex = 0 ; agents ! = null & & agentIndex < agents . length ( ) ; agentIndex + + ) {
if ( ! sessionName . equals ( agentSessionName ( agents . optJSONObject ( agentIndex ) ) ) ) {
continue ;
}
if ( projectName . equals ( targetGroup ) ) {
alreadyInTarget = true ;
} else {
api . removeKanbanSession ( projectName , sessionName , false ) ;
}
}
}
if ( targetGroup ! = null & & ! alreadyInTarget ) {
api . addKanbanSession ( targetGroup , sessionName ) ;
}
} ) ;
}
private void moveSessionBetweenGroups ( String sessionName , String sourceGroup , String targetGroup ) {
if ( ( sourceGroup = = null & & targetGroup = = null )
| | ( sourceGroup ! = null & & sourceGroup . equals ( targetGroup ) ) ) {
return ;
}
runApiAction ( " Move session " , ( ) - > {
if ( sourceGroup ! = null ) {
api . removeKanbanSession ( sourceGroup , sessionName , false ) ;
}
if ( targetGroup ! = null ) {
api . addKanbanSession ( targetGroup , sessionName ) ;
}
} ) ;
}
private void promptAddKanbanSessionToProject ( String projectName ) {
@@ -2820,7 +3226,7 @@ public final class MainActivity extends Activity {
EditText project = formField ( form , " Project " , " " ) ;
EditText agent = formField ( form , " Agent/session name " , " " ) ;
EditText kill = formField ( form , " Kill too: true or false " , " false " ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Remove kanban session " )
. setView ( form )
. setNegativeButton ( " Cancel " , null )
@@ -3140,7 +3546,7 @@ public final class MainActivity extends Activity {
EditText body = formField ( form , " Message " , " " ) ;
body . setMinLines ( 3 ) ;
body . setSingleLine ( false ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Send group message " )
. setView ( form )
. setNegativeButton ( " Cancel " , null )
@@ -3161,7 +3567,7 @@ public final class MainActivity extends Activity {
LinearLayout form = formRoot ( ) ;
EditText project = formField ( form , " Project " , " " ) ;
EditText message = formField ( form , " Message ID " , " " ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Scan group message " )
. setView ( form )
. setNegativeButton ( " Cancel " , null )
@@ -3186,7 +3592,7 @@ public final class MainActivity extends Activity {
EditText body = formField ( form , " Body " , " " ) ;
body . setMinLines ( 2 ) ;
body . setSingleLine ( false ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Post hook event " )
. setView ( form )
. setNegativeButton ( " Cancel " , null )
@@ -3205,7 +3611,7 @@ public final class MainActivity extends Activity {
LinearLayout form = formRoot ( ) ;
EditText session = formField ( form , " Session optional " , " " ) ;
EditText url = formField ( form , " Image URL " , " " ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Upload image URL " )
. setView ( form )
. setNegativeButton ( " Cancel " , null )
@@ -3222,7 +3628,7 @@ public final class MainActivity extends Activity {
EditText session = new EditText ( this ) ;
session . setSingleLine ( true ) ;
session . setHint ( " session optional " ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Upload image file " )
. setView ( session )
. setNegativeButton ( " Cancel " , null )
@@ -3248,7 +3654,7 @@ public final class MainActivity extends Activity {
LinearLayout form = formRoot ( ) ;
EditText path = formField ( form , " Path " , " " ) ;
EditText basePath = formField ( form , " Base path optional " , " " ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Image preview info " )
. setView ( form )
. setNegativeButton ( " Cancel " , null )
@@ -3265,7 +3671,7 @@ public final class MainActivity extends Activity {
LinearLayout form = formRoot ( ) ;
EditText path = formField ( form , " Path " , " " ) ;
EditText basePath = formField ( form , " Base path optional " , " " ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Open image preview " )
. setView ( form )
. setNegativeButton ( " Cancel " , null )
@@ -3300,7 +3706,7 @@ public final class MainActivity extends Activity {
text . append ( '\n' ) ;
text . append ( permissionSummary ( ) ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Permissions / update " )
. setMessage ( text . toString ( ) )
. setNegativeButton ( " Close " , null )
@@ -3338,7 +3744,7 @@ public final class MainActivity extends Activity {
" Preview manual: " + BuildConfig . DEFAULT_PREVIEW_UPDATE_URL ,
" Custom URL "
} ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Update source " )
. setItems ( items , ( dialog , which ) - > {
if ( which = = 0 ) {
@@ -3473,7 +3879,7 @@ public final class MainActivity extends Activity {
image . setImageBitmap ( bitmap ) ;
image . setAdjustViewBounds ( true ) ;
image . setPadding ( dp ( 8 ) , dp ( 8 ) , dp ( 8 ) , dp ( 8 ) ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( " Image preview " )
. setView ( image )
. setPositiveButton ( " Close " , null )
@@ -3511,7 +3917,7 @@ public final class MainActivity extends Activity {
input . setHint ( hint ) ;
input . setText ( value ) ;
input . setSelectAllOnFocus ( true ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( title )
. setView ( input )
. setNegativeButton ( " Cancel " , null )
@@ -3569,7 +3975,7 @@ public final class MainActivity extends Activity {
ScrollView scroll = new ScrollView ( this ) ;
TextView content = new TextView ( this ) ;
content . setText ( text = = null | | text . isEmpty ( ) ? " (empty) " : text ) ;
content . setTextColor ( Color . rgb ( 20 , 24 , 30 ) ) ;
content . setTextColor ( COLOR_TEXT ) ;
content . setTextSize ( 12 ) ;
content . setTypeface ( Typeface . MONOSPACE ) ;
content . setTextIsSelectable ( true ) ;
@@ -3578,13 +3984,150 @@ public final class MainActivity extends Activity {
ViewGroup . LayoutParams . MATCH_PARENT ,
ViewGroup . LayoutParams . WRAP_CONTENT
) ) ;
new AlertDialog . Builder ( this )
new AppDialogBuilder ( )
. setTitle ( title )
. setView ( scroll )
. setPositiveButton ( " Close " , null )
. show ( ) ;
}
private final class AppDialogBuilder {
private String title = " " ;
private String message ;
private View content ;
private String [ ] items ;
private DialogInterface . OnClickListener itemAction ;
private String negativeLabel ;
private DialogInterface . OnClickListener negativeAction ;
private String neutralLabel ;
private DialogInterface . OnClickListener neutralAction ;
private String positiveLabel ;
private DialogInterface . OnClickListener positiveAction ;
AppDialogBuilder setTitle ( String value ) {
title = value ;
return this ;
}
AppDialogBuilder setMessage ( String value ) {
message = value ;
return this ;
}
AppDialogBuilder setView ( View value ) {
content = value ;
return this ;
}
AppDialogBuilder setItems ( String [ ] values , DialogInterface . OnClickListener action ) {
items = values ;
itemAction = action ;
return this ;
}
AppDialogBuilder setNegativeButton ( String label , DialogInterface . OnClickListener action ) {
negativeLabel = label ;
negativeAction = action ;
return this ;
}
AppDialogBuilder setNeutralButton ( String label , DialogInterface . OnClickListener action ) {
neutralLabel = label ;
neutralAction = action ;
return this ;
}
AppDialogBuilder setPositiveButton ( String label , DialogInterface . OnClickListener action ) {
positiveLabel = label ;
positiveAction = action ;
return this ;
}
Dialog show ( ) {
Dialog dialog = new Dialog ( MainActivity . this ) ;
LinearLayout sheet = bottomSheetContent ( dialog , title , dialogSubtitle ( ) ) ;
if ( message ! = null ) {
TextView body = bodyText ( message ) ;
body . setTextSize ( 12 ) ;
body . setTextIsSelectable ( true ) ;
body . setPadding ( dp ( 2 ) , dp ( 4 ) , dp ( 2 ) , dp ( 8 ) ) ;
sheet . addView ( body , matchWrap ( ) ) ;
}
if ( content ! = null ) {
LinearLayout . LayoutParams contentParams = matchWrap ( ) ;
contentParams . bottomMargin = dp ( 10 ) ;
sheet . addView ( content , contentParams ) ;
}
if ( items ! = null ) {
for ( int index = 0 ; index < items . length ; index + + ) {
final int selected = index ;
Button choice = toolbarButton ( items [ index ] , view - > {
dialog . dismiss ( ) ;
if ( itemAction ! = null ) {
itemAction . onClick ( dialog , selected ) ;
}
} ) ;
choice . setGravity ( Gravity . START | Gravity . CENTER_VERTICAL ) ;
choice . setTextSize ( 11 ) ;
LinearLayout . LayoutParams choiceParams = matchWrap ( ) ;
choiceParams . height = dp ( 42 ) ;
choiceParams . bottomMargin = dp ( 6 ) ;
sheet . addView ( choice , choiceParams ) ;
}
}
addDialogActions ( dialog , sheet ) ;
showBottomSheet ( dialog , sheet , null ) ;
return dialog ;
}
private String dialogSubtitle ( ) {
if ( items ! = null ) {
return " Choose an option " ;
}
if ( content ! = null ) {
return " Complete the fields below " ;
}
return " Confirm this action " ;
}
private void addDialogActions ( Dialog dialog , LinearLayout sheet ) {
if ( negativeLabel = = null & & neutralLabel = = null & & positiveLabel = = null ) {
return ;
}
LinearLayout actions = new LinearLayout ( MainActivity . this ) ;
actions . setOrientation ( LinearLayout . HORIZONTAL ) ;
actions . setGravity ( Gravity . END | Gravity . CENTER_VERTICAL ) ;
addDialogAction ( actions , dialog , neutralLabel , neutralAction , DialogInterface . BUTTON_NEUTRAL , false ) ;
addDialogAction ( actions , dialog , negativeLabel , negativeAction , DialogInterface . BUTTON_NEGATIVE , false ) ;
addDialogAction ( actions , dialog , positiveLabel , positiveAction , DialogInterface . BUTTON_POSITIVE ,
" Remove " . equals ( positiveLabel ) | | " Kill " . equals ( positiveLabel ) | | " Delete " . equals ( positiveLabel ) ) ;
sheet . addView ( actions , spacedMatchWrap ( 4 , 0 ) ) ;
}
private void addDialogAction (
LinearLayout actions ,
Dialog dialog ,
String label ,
DialogInterface . OnClickListener action ,
int which ,
boolean danger
) {
if ( label = = null ) {
return ;
}
Button button = compactButton ( label , view - > {
dialog . dismiss ( ) ;
if ( action ! = null ) {
action . onClick ( dialog , which ) ;
}
} ) ;
if ( danger ) {
button . setTextColor ( COLOR_DANGER ) ;
}
actions . addView ( button ) ;
}
}
private LinearLayout formRoot ( ) {
LinearLayout form = new LinearLayout ( this ) ;
form . setOrientation ( LinearLayout . VERTICAL ) ;
@@ -3963,6 +4506,9 @@ public final class MainActivity extends Activity {
normalized = normalized + TERMINAL_ENTER ;
}
terminalFollowOutput = true ;
if ( activeSessionName ! = null ) {
upsertAgentActivity ( ConversationMessage . localUser ( activeSessionName , text ) ) ;
}
sendTerminalInput ( normalized ) ;
inputField . setText ( " " ) ;
setStatus ( " Sent " + text . length ( ) + " chars " ) ;
@@ -4781,6 +5327,7 @@ public final class MainActivity extends Activity {
return ;
}
if ( " conversation-message " . equals ( type ) ) {
upsertAgentActivity ( ConversationMessage . fromJson ( event ) ) ;
return ;
}
if ( " hook-event " . equals ( type ) ) {
@@ -4921,6 +5468,16 @@ public final class MainActivity extends Activity {
void onValue ( int value ) ;
}
private static final class SessionDrag {
final String sessionName ;
final String sourceGroup ;
SessionDrag ( String sessionName , String sourceGroup ) {
this . sessionName = sessionName ;
this . sourceGroup = sourceGroup ;
}
}
private static final class SheetAction {
final String icon ;
final String title ;