Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
95bb913
feat(mobile-client,mobile): side chats and subagents — core + FFI (an…
hoangvu12 Oct 15, 2026
5bbd297
fix(mobile-client): the lost SessionHandle::subagents/is_subagent edi…
hoangvu12 Oct 15, 2026
3d10c3b
test(mobile-client): the background-subagent binding (claude-code run…
hoangvu12 Oct 15, 2026
ded3ae6
Merge remote-tracking branch 'origin/main' into android/next-06-side-…
hoangvu12 Oct 15, 2026
e13da95
fix(mobile-client): side chats and subagents compile and match the en…
hoangvu12 Oct 15, 2026
09df3f7
fix(mobile): FFI defaults for the side-chat fields; card link titles …
hoangvu12 Oct 15, 2026
8ce6222
feat(android): spawn cards open the subagent's live read-only transcr…
hoangvu12 Oct 15, 2026
a19ba3d
feat(android): Side Chats & Agents sheet, header button, New Side Cha…
hoangvu12 Oct 15, 2026
c7f6ec9
feat(android): running-agents pill on session rows (android-next#06)
hoangvu12 Oct 15, 2026
e804740
test(android): side chats, subagent links, the pill and the side-chat…
hoangvu12 Oct 15, 2026
aa8f0d6
fix(android): ticket 06 static compile pass
hoangvu12 Oct 15, 2026
f06be6b
fix(mobile-client): close the background-subagent test's outer any()
hoangvu12 Oct 15, 2026
53433fc
merge: origin/main (PR #20, PR #21 agent accounts) into android-next#06
hoangvu12 Oct 15, 2026
1ffe097
fix(mobile): SubagentInfo FFI test clones the row it reuses for the f…
hoangvu12 Oct 15, 2026
2a95044
test(mobile-client): side-chat rig keeps its EngineListener alive
hoangvu12 Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ internal fun NewSessionCanvas(
state: NewSessionCanvasState = remember { NewSessionCanvasState() },
insets: CanvasInsets? = null,
wallpaper: @Composable (Modifier, () -> Rect?) -> Unit = { m, c -> WallpaperHero(m, cutout = c) },
/** android-next #06: a side chat's draft says "Ask on the side". */
headline: String = NewSessionHeadline,
/** android-next #06: false when the host draws its own bar (the side-chat draft's session header). */
navBar: Boolean = true,
) {
ProvideHazeState {
val haze = LocalHazeState.current!!
Expand Down Expand Up @@ -191,6 +195,7 @@ internal fun NewSessionCanvas(
Modifier
.hazeSource(haze, zIndex = 1f)
.testTag("new-session-hero"),
headline = headline,
)
}
val composerSlot: @Composable () -> Unit = {
Expand Down Expand Up @@ -323,7 +328,7 @@ internal fun NewSessionCanvas(
},
)
}
NewSessionNavBar(embedded, onClose, insets)
if (navBar) NewSessionNavBar(embedded, onClose, insets)
}
}
}
Expand All @@ -337,7 +342,7 @@ internal fun NewSessionCanvas(
* the column is 76.6 dp (1 line) or 105.2 dp (2 lines) at font scale 1.
*/
@Composable
internal fun NewSessionHero(harness: String, modifier: Modifier = Modifier) {
internal fun NewSessionHero(harness: String, modifier: Modifier = Modifier, headline: String = NewSessionHeadline) {
val palette = RobocoTheme.palette
val typeScale = LocalTypeScale.current
Column(
Expand All @@ -354,7 +359,7 @@ internal fun NewSessionHero(harness: String, modifier: Modifier = Modifier) {
.clearAndSetSemantics { },
)
BasicText(
"What are we building?",
headline,
Modifier.testTag("new-session-headline"),
style = uiStyle(Face.SansSemibold, typeScale.fixed(NewSessionLayout.HeadlineSize), palette.text)
.copy(textAlign = TextAlign.Center),
Expand All @@ -364,6 +369,12 @@ internal fun NewSessionHero(harness: String, modifier: Modifier = Modifier) {
}
}

/** The New Session hero's headline (`:122`). */
const val NewSessionHeadline = "What are we building?"

/** A side chat's draft headline (upstream #899's `sideOf` hero). */
const val SideChatHeadline = "Ask on the side"

/**
* The transparent nav bar (`:113-118`, `SplitRootController.swift:46-47`).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ fun newSessionChips(
return chips
}

/** The chips a side chat's draft keeps (iOS `refreshChips` with `sideOf`). */
private val SideChatChipIds = setOf(NewSessionChipIds.Model, NewSessionChipIds.Effort)

/**
* Foundation's `String.capitalized` (`CFStringCapitalize`), which the effort
* titles (here and in ticket 114's effort rows) use: a letter is
Expand Down Expand Up @@ -262,13 +265,16 @@ fun NewSessionChipsEffect(
) {
val chips by remember(model, labels) {
derivedStateOf {
newSessionChips(
val all = newSessionChips(
model.draft,
model.project,
model.currentOptions?.hosts.orEmpty(),
model.models,
labels,
)
// android-next #06: a side chat runs in its parent's checkout
// on its parent's host — only the model and effort are picked.
if (model.side == null) all else all.filter { it.id in SideChatChipIds }
}
}
// Seed synchronously so the first composition already has the chips.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ internal object NewSessionRequests {
* neither → `null`, and the caller fails **without** a core call (the
* alert explains). Ids are the scoped ids the options carry.
*/
fun target(d: NewSessionDraft): SessionTarget? {
fun target(d: NewSessionDraft, side: SideChatParent? = null): SessionTarget? {
// android-next #06: ignored by the core for a side chat (the
// parent's host fills it), but the record needs one (iOS `sideOf`).
if (side != null) return SessionTarget.Projectless(deviceId = side.deviceId)
val projectId = d.projectId
if (projectId != null) return SessionTarget.Project(spaceId = projectId)
val hostId = d.hostId
Expand All @@ -89,9 +92,11 @@ internal object NewSessionRequests {
* `NewSession(target:config:branch:cwd:title:)` (`:629-630`):
* `reasoning = effort` (nil ⇒ the engine chooses), `sandbox` is
* always workspace-write on this canvas, `branch` only without a
* worktree (with one, the branch becomes the worktree's base).
* worktree (with one, the branch becomes the worktree's base). A side
* chat (android-next #06) names its parent and no branch: it runs in
* the parent's checkout (the core keeps the parent's sandbox too).
*/
fun newSession(d: NewSessionDraft, target: SessionTarget): NewSession = NewSession(
fun newSession(d: NewSessionDraft, target: SessionTarget, side: SideChatParent? = null): NewSession = NewSession(
target = target,
config = ChatConfig(
harness = d.harness,
Expand All @@ -100,9 +105,10 @@ internal object NewSessionRequests {
modelOptions = emptyMap(),
sandbox = SandboxLevel.WORKSPACE_WRITE,
),
branch = if (d.worktree) null else d.branch,
branch = if (d.worktree || side != null) null else d.branch,
cwd = null,
title = null,
parentChatId = side?.chatId,
)

/**
Expand All @@ -117,6 +123,7 @@ internal object NewSessionRequests {
text: String,
images: List<StagedImage>,
projects: List<ProjectOption>,
side: SideChatParent? = null,
): SendRequest = SendRequest(
text = text,
attachments = images.map { image ->
Expand All @@ -127,7 +134,8 @@ internal object NewSessionRequests {
data = image.bytes,
)
},
worktree = if (d.worktree) {
// A side chat never makes a worktree (iOS `draft.worktree && parentId == nil`).
worktree = if (d.worktree && side == null) {
projects.firstOrNull { it.id == d.projectId }?.let { project ->
WorktreeSpec(
repoPath = project.path,
Expand Down Expand Up @@ -170,14 +178,15 @@ class NewSessionCreator(
text: String,
images: List<StagedImage>,
projects: List<ProjectOption>,
side: SideChatParent? = null,
): String? {
// No target: fail without a core call and without a log — the
// alert explains it, and there is no error to log.
val target = NewSessionRequests.target(d) ?: return null
val target = NewSessionRequests.target(d, side) ?: return null
return withContext(io) {
try {
val chatId = core.createSession(NewSessionRequests.newSession(d, target))
core.sendFirst(chatId, NewSessionRequests.sendRequest(d, text, images, projects))
val chatId = core.createSession(NewSessionRequests.newSession(d, target, side))
core.sendFirst(chatId, NewSessionRequests.sendRequest(d, text, images, projects, side))
chatId
} catch (cancelled: CancellationException) {
throw cancelled
Expand All @@ -201,6 +210,17 @@ val StartFailedAlert = RobocoAlert(
buttons = listOf(AlertButton("OK", AlertButtonRole.Default)),
)

/**
* A side chat's failure alert (android-next #06, upstream #899's
* `create(sideOf:)` copy verbatim): the parent is gone or the create
* failed — the composer stays `Kept`.
*/
val SideChatStartFailedAlert = RobocoAlert(
title = "Couldn't start the side chat",
message = "Its session is no longer available.",
buttons = listOf(AlertButton("OK", AlertButtonRole.Default)),
)

/**
* The canvas composer's offline gate (94's Roboco ADD, the parent
* spec's "Send is disabled while the target engine is offline"): the
Expand Down
Loading
Loading