Skip to content

Commit 37c31c5

Browse files
authored
Merge pull request #9 from tall-1997/feature/chat-pill-menu-drawer
会话页 PR-3 + PR-6:单行工具胶囊、菜单分组、紧凑宽度会话抽屉
2 parents 662e6d3 + 67fc2cb commit 37c31c5

29 files changed

Lines changed: 654 additions & 680 deletions

File tree

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatAssistantMessageUI.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ import androidx.compose.ui.graphics.toArgb
7878
import androidx.compose.ui.platform.LocalConfiguration
7979
import androidx.compose.ui.platform.LocalContext
8080
import androidx.compose.ui.res.stringResource
81+
import androidx.compose.ui.text.style.TextOverflow
8182
import androidx.compose.ui.text.font.FontFamily
8283
import androidx.compose.ui.text.font.FontWeight
83-
import androidx.compose.ui.text.style.TextOverflow
8484
import androidx.compose.ui.unit.dp
8585
import androidx.compose.foundation.layout.offset
8686
import androidx.compose.foundation.rememberScrollState

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatComposerWidgets.kt‎

Lines changed: 117 additions & 517 deletions
Large diffs are not rendered by default.

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatFlatItems.kt‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,33 @@ internal fun isFloatingProcessTool(
968968
return true
969969
}
970970

971+
/**
972+
* [P0-4] The blocks the floating activity pill may pin, derived from the
973+
* LATEST assistant reply. ONE pure projection shared by the pill's mount
974+
* gate (hasFloatingTools in ChatScreen, synchronous) and its data pipeline
975+
* (lastToolBlocks, coroutine-side) — previously two hand-maintained filters
976+
* that could drift: the gate scanned ALL messages while the pipeline read
977+
* only the latest reply, and neither knew about the showFloatingToolBar
978+
* pref or the streaming state, which left a 79dp bottom reserve for an
979+
* overlay that never rendered after a fold-OFF turn ended.
980+
*
981+
* Fold-ON returns empty (single channel: the running round renders every
982+
* step in-list). Sub-agent transcript cards are dropped while the
983+
* sub-agent bar owns their display.
984+
*/
985+
internal fun floatingOverlayBlocks(
986+
latestReply: ChatMessage?,
987+
foldAiProcess: Boolean,
988+
showSubAgentBar: Boolean,
989+
activeSubAgentToolIds: Set<String>,
990+
): List<AssistantBlock> = latestReply?.toolBlocks.orEmpty()
991+
.filter { isFloatingProcessTool(it, foldAiProcess) }
992+
.filterNot {
993+
showSubAgentBar && isSubAgentTranscriptCard(it) &&
994+
(it.id in activeSubAgentToolIds ||
995+
it.id.substringBefore("#sub-") in activeSubAgentToolIds)
996+
}
997+
971998
// ── [P1-3] Row rhythm ───────────────────────────────────────────────────
972999
//
9731000
// Vertical spacing between list rows, replacing the flat LazyColumn

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatMiscViews.kt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,6 @@ private fun parseInlineMarkdown(
676676
// [T-android-split-chat] internal (was private) — referenced from ChatScreen.kt
677677
// after the move.
678678
internal val LocalBrowserTabPool = compositionLocalOf<com.openminis.app.browser.BrowserTabPool?> { null }
679-
internal val LocalToolPreviewEnabled = compositionLocalOf { true }
680679

681680
@Composable
682681
internal fun rememberBrowserLiveSnapshot(

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatScreen.kt‎

Lines changed: 165 additions & 137 deletions
Large diffs are not rendered by default.

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatScreenHelpers.kt‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,32 @@ package com.openminis.app.ui.chat
77
import android.net.Uri
88
import android.provider.OpenableColumns
99
import java.io.File
10+
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
11+
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
12+
import androidx.window.core.layout.WindowSizeClass
13+
import androidx.compose.runtime.Composable
1014
import androidx.compose.runtime.getValue
1115
import androidx.compose.runtime.setValue
1216

17+
/**
18+
* [P0-6] Whether the chat top bar has room for the three high-frequency
19+
* quick actions (terminal / browser / chat files) as direct icons beside
20+
* the "..." menu. Width ≥ 600dp (MEDIUM) — below that the centered
21+
* two-line title (session title + provider·model row) needs the horizontal
22+
* budget, and the menu's 快捷 section carries the entries instead.
23+
*
24+
* Same window-size source as `shouldUseTwoPane()` in ChatSplitScaffold
25+
* (currentWindowAdaptiveInfo, NOT LocalConfiguration.screenWidthDp — the
26+
* latter measures the display, so in split-screen/freeform windows the
27+
* predicate would answer for a window the app does not have).
28+
*/
29+
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
30+
@Composable
31+
internal fun shouldShowTopBarQuickActions(): Boolean =
32+
currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(
33+
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND,
34+
)
35+
1336
/**
1437
* Create an empty JPEG file under filesDir/camera-photos/ and return a
1538
* FileProvider content URI for it, plus the underlying File. The camera

‎src/android/app/src/main/java/com/openminis/app/ui/chat/ChatSubAgentBar.kt‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,12 @@ internal fun subAgentToolTitle(name: String, args: String): String {
363363
"file_read" -> "读取文件"
364364
"file_write" -> "写入文件"
365365
"file_edit", "multi_edit" -> "编辑文件"
366-
"grep_source" -> "搜索代码"
367-
"web_search" -> "搜索网页"
366+
"grep_source", "grep" -> "搜索代码"
367+
"web_search", "clientfn_web_search" -> "搜索网页"
368+
"web_fetch" -> "抓取网页"
368369
"browser_use" -> "浏览器"
370+
"memory_get", "memory_write" -> "记忆"
371+
"spawn_agent", "dispatch_agents" -> "子代理"
369372
else -> name
370373
}
371374
val preview = if (args.isBlank()) "" else SubAgentRunner.previewToolArgs(args).take(42)

‎src/android/app/src/main/java/com/openminis/app/ui/components/MinisMenu.kt‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import androidx.compose.foundation.layout.ColumnScope
1414
import androidx.compose.foundation.layout.IntrinsicSize
1515
import androidx.compose.foundation.layout.PaddingValues
1616
import androidx.compose.foundation.layout.padding
17+
import androidx.compose.foundation.layout.Row
18+
import androidx.compose.material3.Text
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.text.font.FontWeight
21+
import androidx.compose.ui.unit.sp
22+
import androidx.compose.ui.unit.dp
1723
import androidx.compose.foundation.layout.width
1824
import androidx.compose.foundation.layout.widthIn
1925
import androidx.compose.foundation.rememberScrollState
@@ -247,3 +253,27 @@ object MinisMenuDefaults {
247253
trailingIconColor = MaterialTheme.colorScheme.error,
248254
)
249255
}
256+
257+
/**
258+
* [P0-6] Small group label for a [MinisMenu] section. The chat top-bar menu
259+
* used to be one flat 16+ item list; sections (快捷 / 会话 / 工具 / 设置)
260+
* are what make it scannable. A header rather than another divider: a
261+
* divider only says "break", a label says why.
262+
*/
263+
@Composable
264+
fun MinisMenuSectionHeader(
265+
title: String,
266+
modifier: Modifier = Modifier,
267+
) {
268+
Row(
269+
modifier = modifier.padding(start = 16.dp, end = 16.dp, top = 10.dp, bottom = 2.dp),
270+
verticalAlignment = Alignment.CenterVertically,
271+
) {
272+
Text(
273+
text = title,
274+
fontSize = 11.sp,
275+
fontWeight = FontWeight.SemiBold,
276+
color = MaterialTheme.colorScheme.onSurfaceVariant,
277+
)
278+
}
279+
}

‎src/android/app/src/main/java/com/openminis/app/ui/navigation/ChatSplitScaffold.kt‎

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ import androidx.compose.material.icons.filled.Menu
2020
import androidx.compose.material3.Icon
2121
import androidx.compose.material3.IconButton
2222
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.ModalDrawerSheet
24+
import androidx.compose.material3.ModalNavigationDrawer
2325
import androidx.compose.material3.Surface
2426
import androidx.compose.material3.Text
27+
import androidx.compose.material3.rememberDrawerState
28+
import androidx.compose.material3.DrawerValue
2529
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
2630
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
2731
import androidx.compose.material3.adaptive.currentWindowSize
@@ -287,6 +291,15 @@ fun ChatSplitScaffold(
287291
*/
288292
draftPlaceholderId: String?,
289293
onSessionSelected: (String) -> Unit,
294+
/**
295+
* [P1-6] True when the list is drawn inside the compact-width modal
296+
* drawer rather than as a full screen or the two-pane sidebar. The
297+
* list drops its own top bar then — the drawer sheet carries the
298+
* chrome. Same composable either way; the pane navigator stays the
299+
* single owner of list-vs-detail, which is what keeps chat switching
300+
* identical across the three presentations.
301+
*/
302+
embedded: Boolean,
290303
) -> Unit,
291304
detailPane: @Composable (
292305
sessionId: String,
@@ -301,6 +314,13 @@ fun ChatSplitScaffold(
301314
onToggleSidebar: (() -> Unit)?,
302315
/** Whether the list is currently hidden — picks the icon direction. */
303316
sidebarCollapsed: Boolean,
317+
/**
318+
* [P1-6] Open the compact-width session drawer, or null in two-pane
319+
* (the sidebar is already the list). The drawer state lives inside
320+
* [ChatSplitScaffold], so the slot hands the opener to the detail
321+
* pane instead of the pane reaching into scaffold internals.
322+
*/
323+
onOpenSessionDrawer: (() -> Unit)?,
304324
) -> Unit,
305325
) {
306326
val twoPane = shouldUseTwoPane()
@@ -455,6 +475,13 @@ fun ChatSplitScaffold(
455475
)
456476
val scope = rememberCoroutineScope()
457477

478+
// [P1-6] Compact-width session drawer. State lives here, inside the
479+
// navigator's key block, so a window-size rebuild (rotation, fold,
480+
// split-screen) starts the drawer closed rather than resurrecting it
481+
// over a layout that may now be two-pane.
482+
val sessionDrawerState = rememberDrawerState(DrawerValue.Closed)
483+
val openSessionDrawer: () -> Unit = { scope.launch { sessionDrawerState.open() } }
484+
458485
val currentSessionId = navigator.currentDestination
459486
?.takeIf { it.pane == ListDetailPaneScaffoldRole.Detail }
460487
?.contentKey
@@ -509,6 +536,53 @@ fun ChatSplitScaffold(
509536
// Deliberately NOT focusable itself: preview events travel down from the
510537
// focus owner's ancestors, so this sees the chord without competing for
511538
// focus with the composer's text field.
539+
//
540+
// [P1-6] Compact-width session drawer. Wraps the WHOLE list/detail pair
541+
// (including the two-pane layout) so the gesture surface is one thing at
542+
// every window size: edge-swipe opens it, but the sheet only mounts while
543+
// single-pane — at ≥840dp the persistent sidebar already shows the list
544+
// and a second copy would be two lists of the same sessions. Switching a
545+
// chat from the drawer goes through the SAME pane navigator as the
546+
// full-screen list and the two-pane sidebar (see the selection callback
547+
// below), so the three presentations cannot drift.
548+
ModalNavigationDrawer(
549+
drawerState = sessionDrawerState,
550+
gesturesEnabled = !twoPane,
551+
drawerContent = {
552+
if (!twoPane) {
553+
ModalDrawerSheet(modifier = Modifier.width(320.dp)) {
554+
listPane(
555+
// The drawer sits OVER the chat, so the current chat
556+
// is highlighted — same row the two-pane sidebar marks.
557+
if (currentSessionId != null) {
558+
com.openminis.app.ui.chat.ChatViewModelStore
559+
.rememberPersistedId(currentSessionId)
560+
} else null,
561+
if (currentSessionId != null &&
562+
isDraftSessionId(
563+
com.openminis.app.ui.chat.ChatViewModelStore
564+
.rememberPersistedId(currentSessionId),
565+
)
566+
) currentSessionId else null,
567+
{ sessionId ->
568+
selectedSessionId = sessionId
569+
scope.launch {
570+
navigator.navigateTo(
571+
ListDetailPaneScaffoldRole.Detail,
572+
sessionId,
573+
)
574+
sessionDrawerState.close()
575+
}
576+
},
577+
// [P1-6] Drawer presentation — positional args: the
578+
// listPane slot is a function TYPE, whose call sites
579+
// cannot use named arguments.
580+
true,
581+
)
582+
}
583+
}
584+
},
585+
) {
512586
Box(
513587
Modifier
514588
.fillMaxSize()
@@ -559,12 +633,17 @@ fun ChatSplitScaffold(
559633
.rememberPersistedId(currentSessionId),
560634
)
561635
) currentSessionId else null,
562-
) { sessionId ->
563-
selectedSessionId = sessionId
564-
scope.launch {
565-
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, sessionId)
566-
}
567-
}
636+
{ sessionId ->
637+
selectedSessionId = sessionId
638+
scope.launch {
639+
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, sessionId)
640+
}
641+
},
642+
// Full screen / sidebar presentation — own top bar.
643+
// Positional: the listPane slot is a function TYPE, whose
644+
// call sites cannot use named arguments.
645+
false,
646+
)
568647
}
569648
},
570649
detailPane = {
@@ -653,6 +732,9 @@ fun ChatSplitScaffold(
653732
null
654733
},
655734
listCollapsed,
735+
// [P1-6] Drawer opener for compact width; null when
736+
// the sidebar already is the list.
737+
if (twoPane) null else openSessionDrawer,
656738
)
657739
}
658740
}
@@ -714,6 +796,7 @@ fun ChatSplitScaffold(
714796
}
715797
}
716798
}
799+
}
717800
}
718801

719802
/**
@@ -882,7 +965,7 @@ fun ChatSplitScaffoldRoute(
882965
) {
883966
ChatSplitScaffold(
884967
initialSessionId = initialSessionId,
885-
listPane = { selectedSessionId, draftPlaceholderId, onSessionSelected ->
968+
listPane = { selectedSessionId, draftPlaceholderId, onSessionSelected, embedded ->
886969
com.openminis.app.ui.sessions.SessionListScreen(
887970
chatRepository = chatRepository,
888971
providerRepository = providerRepository,
@@ -903,10 +986,12 @@ fun ChatSplitScaffoldRoute(
903986
// [T-android-draft-placeholder-row] Synthetic "New Chat" row,
904987
// never persisted — see the listPane param docs.
905988
draftPlaceholderId = draftPlaceholderId,
989+
// [P1-6] Drawer presentation drops the list's own top bar.
990+
embedded = embedded,
906991
)
907992
},
908993
detailPane = { sessionId, onBackInPane, onNewChatInPane, onMoveToInPane,
909-
onToggleSidebar, sidebarCollapsed ->
994+
onToggleSidebar, sidebarCollapsed, onOpenSessionDrawer ->
910995
com.openminis.app.ui.chat.ChatScreen(
911996
// The pane navigator's contentKey IS the session id, so a
912997
// draft ("__new__…") flows through unchanged and ChatScreen's
@@ -930,6 +1015,9 @@ fun ChatSplitScaffoldRoute(
9301015
// looks for "get me back to the list".
9311016
onToggleSidebar = onToggleSidebar,
9321017
sidebarCollapsed = sidebarCollapsed,
1018+
// [P1-6] Compact width only — the drawer is not composed in
1019+
// two-pane (the sidebar is already the list).
1020+
onOpenSessionDrawer = onOpenSessionDrawer,
9331021
// Draft handling: the pane navigator gets a fresh `__new__…`
9341022
// key, so the list keeps its scroll position and simply stops
9351023
// highlighting a row (no persisted id matches a draft) — the

‎src/android/app/src/main/java/com/openminis/app/ui/sessions/SessionListScreen.kt‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,13 @@ fun SessionListScreen(
501501
* prepends the same placeholder in split mode.
502502
*/
503503
draftPlaceholderId: String? = null,
504+
/**
505+
* [P1-6] Render the list WITHOUT its own top bar, for embedding inside
506+
* the compact-width session drawer (ChatSplitScaffold owns the drawer
507+
* chrome and the close affordance). Default false keeps the full-screen
508+
* and two-pane-sidebar renderings byte-identical.
509+
*/
510+
embedded: Boolean = false,
504511
) {
505512
val context = LocalContext.current
506513
// T46: hoist VM ownership to the NavBackStackEntry's ViewModelStore so
@@ -778,7 +785,7 @@ fun SessionListScreen(
778785

779786
Scaffold(
780787
topBar = {
781-
MinisCenterTopBar(
788+
if (!embedded) MinisCenterTopBar(
782789
title = {
783790
Text(
784791
stringResource(R.string.app_name),

0 commit comments

Comments
 (0)