Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -276,7 +276,7 @@ internal fun CreateSessionScreen(
label = when {
workspaceState is RemoteWorkspaceUiState.Loading -> stringResource(R.string.sessions_loading)
workspacePath.isEmpty() -> stringResource(R.string.create_chat)
else -> ready?.workspaces?.firstOrNull { it.path == workspacePath }?.name.orEmpty()
else -> ready?.workspaces?.firstOrNull { it.path == workspacePath }?.displayName.orEmpty()
.ifEmpty { workspacePath }
},
expanded = pickerKind == CreateSelectionKind.WORKSPACE,
Expand All @@ -299,7 +299,7 @@ internal fun CreateSessionScreen(
) {
WorkspacePicker(
workspaces = ready?.workspaces.orEmpty().map {
WorkspaceChoice(path = it.path, name = it.name)
WorkspaceChoice(path = it.path, name = it.displayName)
},
selectedPath = workspacePath,
showHeader = false,
Expand Down Expand Up @@ -380,7 +380,7 @@ internal fun CreateSessionScreen(
ModalBottomSheet(onDismissRequest = { pickerKind = null }) {
WorkspacePicker(
workspaces = ready?.workspaces.orEmpty().map {
WorkspaceChoice(path = it.path, name = it.name)
WorkspaceChoice(path = it.path, name = it.displayName)
},
selectedPath = workspacePath,
showHeader = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ internal fun RemoteWorkspacePanel(
TextButton(
onClick = { onIntent(RemoteWorkspaceIntent.SelectWorkspace(workspace.path)) },
enabled = !state.busy && state.selected?.path != workspace.path,
) { Text(workspace.name) }
) { Text(workspace.displayName) }
}
if (state.assistants.isNotEmpty()) {
Text(stringResource(R.string.assistants_title), style = MaterialTheme.typography.titleSmall)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,13 @@ private fun SidebarActiveDeviceBody(
val selected = readyWorkspace.selected
val workspaceRows = buildList {
if (selected != null && selected.path.isNotBlank()) {
add(selected.path to selected.name)
add(selected.path to (readyWorkspace.workspaces.firstOrNull { RemoteWorkspacePathPolicy.equal(it.path, selected.path) }?.displayName ?: selected.name))
}
readyWorkspace.workspaces.forEach { workspace ->
if (workspace.path.isNotBlank() && none {
RemoteWorkspacePathPolicy.equal(it.first, workspace.path)
}) {
add(workspace.path to workspace.name)
add(workspace.path to workspace.displayName)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface InitialSyncResult {
}

export interface RecentWorkspaceEntry {
remoteSshHost?: string;
path: string;
name: string;
lastOpened: string;
Expand Down Expand Up @@ -156,6 +157,7 @@ export interface WorkspaceInfoResponse extends CommandStatusResponse {
}

export interface RecentWorkspaceEntryResponse {
remote_ssh_host?: string;
path?: string;
name?: string;
last_opened?: string | number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export struct SidebarDeviceGroup {
SidebarWorkspaceGroup({
deviceId: this.device.deviceId,
path: entry.path,
name: entry.name,
name: entry.remoteSshHost?.trim() ? `${entry.name} · ${entry.remoteSshHost.trim()}` : entry.name,
sessions: this.sessionsFor(entry.path),
selectedSessionId: this.selectedSessionId,
runningSessionId: this.runningSessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export struct SidebarWorkspacePicker {
.borderRadius(10)
.backgroundColor(CARD)
Column({ space: 3 }) {
Text(workspace.name || this.basename(workspace.path))
Text((workspace.name || this.basename(workspace.path)) + (workspace.remoteSshHost?.trim() ? ` · ${workspace.remoteSshHost.trim()}` : ''))
.width('100%')
.fontSize(MobileDesignTypography.bodyLarge.size)
.fontWeight(workspace.path === this.selectedWorkspacePath ? FontWeight.Medium : FontWeight.Regular)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export class SessionListProjector {
entries.push({
path: inputs.workspacePath,
name: inputs.workspaceName,
remoteSshHost: inputs.recentWorkspaces.find((item: RecentWorkspaceEntry) => item.path === inputs.workspacePath)?.remoteSshHost,
lastOpened: '',
workspaceKind: 'normal'
});
Expand Down Expand Up @@ -355,7 +356,7 @@ export class SessionListProjectionCache {

private static workspaceFingerprint(entries: RecentWorkspaceEntry[]): string[] {
return entries.map((item: RecentWorkspaceEntry): string => {
return `${item.path}${item.name}${item.workspaceKind}`;
return `${item.path}${item.name}${item.workspaceKind}\u0001${item.remoteSshHost || ''}`;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class RemoteResponseMapper {
item.updated_at,
item.updatedAt
]),
remoteSshHost: item.remote_ssh_host,
workspaceKind: item.workspace_kind || 'normal'
};
return workspace;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RemoteResponseMapper } from '../main/ets/services/RemoteResponseMapper';
import { AccountLoginErrorPolicy } from '../main/ets/pages/policy/AccountLoginErrorPolicy';
import { describe, it, expect } from '@ohos/hypium';
import { RemoteI18n } from '../main/ets/i18n/RemoteI18n';
Expand Down Expand Up @@ -55,6 +56,15 @@ interface LoginFailureFixture { code?: number; statusCode?: number; }

export default function conversationPresentationUnitTest() {
describe('HarnessProfilePolicy', () => {
it('workspace_host_survives_mapping_and_legacy_payloads', 0, () => {
const workspaces = RemoteResponseMapper.recentWorkspaces([
{ path: '/remote', name: 'App', remote_ssh_host: '10.0.0.8' },
{ path: '/local', name: 'App' }
]);
expect(workspaces[0].remoteSshHost).assertEqual('10.0.0.8');
expect(workspaces[0].name).assertEqual('App');
expect(workspaces[1].remoteSshHost).assertUndefined();
});
it('maps mobile execution levels to canonical host Agents', 0, () => {
expect(HarnessProfilePolicy.agentType('minimal')).assertEqual('minimal');
expect(HarnessProfilePolicy.agentType('standard')).assertEqual('agentic');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ extension MobileAppModel {
let directory = entry.workspace(path: workspace.path)
return MobileWorkspaceGroup(
path: workspace.path,
name: workspace.name.isEmpty ? workspace.path : workspace.name,
name: workspace.name.isEmpty ? workspace.path : workspace.displayName,
selected: remoteExpectedDeviceKey == deviceKey &&
normalizedSessionWorkspacePath(workspace.path) == normalizedSessionWorkspacePath(workspaceCatalog.first(where: { $0.selected })?.path ?? ""),
sessions: sessions.filter { normalizedSessionWorkspacePath($0.workspacePath ?? "") == normalizedSessionWorkspacePath(workspace.path) },
Expand Down Expand Up @@ -1016,13 +1016,13 @@ extension MobileAppModel {
!selected.path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
let normalizedPath = normalizedSessionWorkspacePath(selected.path)
seen.insert(normalizedPath)
catalog.append((selected.path, selected.name, true))
catalog.append((selected.path, ready.workspaces.first(where: { normalizedSessionWorkspacePath($0.path) == normalizedPath })?.displayName ?? selected.name, true))
}
for workspace in ready.workspaces {
guard !workspace.path.isEmpty else { continue }
let normalizedPath = normalizedSessionWorkspacePath(workspace.path)
guard seen.insert(normalizedPath).inserted else { continue }
catalog.append((workspace.path, workspace.name, false))
catalog.append((workspace.path, workspace.displayName, false))
}
workspaceCatalog = catalog
remoteAssistants = ready.assistants.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ public data class RecentWorkspace public constructor(
public val name: String,
public val lastOpened: String,
public val kind: String,
)
public val remoteSshHost: String?,
) {
public constructor(path: String, name: String, lastOpened: String, kind: String) :
this(path, name, lastOpened, kind, null)
public val displayName: String
get() = remoteSshHost?.trim()?.takeIf { it.isNotEmpty() }?.let { "$name · $it" } ?: name
}

public data class WorkspaceAssistant public constructor(
public val path: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.openbitfun.mobile.core.domain

import kotlin.test.Test
import kotlin.test.assertEquals

class WorkspaceDisplayNameTest {
@Test fun appendsHostWithoutChangingNameOrPath() {
val remote = RecentWorkspace("/app", "App", "", "normal", "10.0.0.8")
assertEquals("App · 10.0.0.8", remote.displayName)
assertEquals("App", remote.name)
assertEquals("/app", remote.path)
assertEquals("App", RecentWorkspace("/app", "App", "", "normal").displayName)
assertEquals("App", remote.copy(remoteSshHost = " ").displayName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public class RemoteWorkspaceStore internal constructor(
name = item.name?.takeIf(String::isNotBlank) ?: basename(item.path.orEmpty()),
lastOpened = item.lastOpened,
kind = item.workspaceKind.orEmpty(),
remoteSshHost = item.remoteSshHost,
)
}.filter { it.path.isNotEmpty() }
val loadedAssistants = assistants.assistants.map { item ->
Expand Down Expand Up @@ -180,6 +181,7 @@ public class RemoteWorkspaceStore internal constructor(
name = item.name?.takeIf(String::isNotBlank) ?: basename(item.path.orEmpty()),
lastOpened = item.lastOpened,
kind = item.workspaceKind.orEmpty(),
remoteSshHost = item.remoteSshHost,
)
}.filter { it.path.isNotEmpty() }
val loadedAssistants = assistants.assistants.map { item ->
Expand Down Expand Up @@ -646,7 +648,7 @@ public class RemoteWorkspaceStore internal constructor(
WorkspaceAssistant(row.path, row.name.ifEmpty { basename(row.path) }, null)
}
val workspaces = rows.filterNot { it.workspaceKind == ASSISTANT_KIND }.map { row ->
RecentWorkspace(row.path, row.name.ifEmpty { basename(row.path) }, row.lastOpened, row.workspaceKind)
RecentWorkspace(row.path, row.name.ifEmpty { basename(row.path) }, row.lastOpened, row.workspaceKind, row.remoteSshHost)
}
return RemoteWorkspaceUiState.Ready(
workspaces = workspaces,
Expand All @@ -664,7 +666,7 @@ public class RemoteWorkspaceStore internal constructor(
assistants: List<WorkspaceAssistant>,
): List<PersistedRemoteWorkspace> {
val rows = workspaces.map { workspace ->
PersistedRemoteWorkspace(workspace.path, workspace.name, workspace.lastOpened, workspace.kind)
PersistedRemoteWorkspace(workspace.path, workspace.name, workspace.lastOpened, workspace.kind, workspace.remoteSshHost)
}.toMutableList()
assistants.forEach { assistant ->
if (rows.none { it.path == assistant.path }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ public data class PersistedRemoteWorkspace public constructor(
public val name: String = "",
public val lastOpened: String = "",
public val workspaceKind: String = "",
public val remoteSshHost: String? = null,
)

public interface RemoteSessionListStore {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class RemotePersistenceStoreTest {
@Test fun workspaceHostSurvivesCacheRoundTripAndLegacyRecords() {
val legacy = Json.decodeFromString<PersistedRemoteWorkspace>("""{"path":"/app","name":"App"}""")
assertEquals(null, legacy.remoteSshHost)
val remote = legacy.copy(remoteSshHost = "10.0.0.8")
assertEquals(remote, Json.decodeFromString<PersistedRemoteWorkspace>(Json.encodeToString(remote)))
}

private suspend fun stores(): Pair<RemoteSessionListStore, RemoteTranscriptStore> {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
MobileDatabase.Schema.create(driver).await()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public data class RecentWorkspaceEntryResponse(
/** Resolved from [RECENT_WORKSPACE_TIME_KEYS]; empty string when absent. */
val lastOpened: String = "",
val workspaceKind: String? = null,
val remoteSshHost: String? = null,
)

public object RecentWorkspaceEntryResponseSerializer : KSerializer<RecentWorkspaceEntryResponse> {
Expand All @@ -54,6 +55,7 @@ public object RecentWorkspaceEntryResponseSerializer : KSerializer<RecentWorkspa
name = json.wireString("name"),
lastOpened = json.firstWireTime(RECENT_WORKSPACE_TIME_KEYS),
workspaceKind = json.wireString("workspace_kind"),
remoteSshHost = json.wireString("remote_ssh_host"),
)
}

Expand All @@ -64,6 +66,7 @@ public object RecentWorkspaceEntryResponseSerializer : KSerializer<RecentWorkspa
value.name?.let { put("name", it) }
if (value.lastOpened.isNotEmpty()) put("last_opened", value.lastOpened)
value.workspaceKind?.let { put("workspace_kind", it) }
value.remoteSshHost?.let { put("remote_ssh_host", it) }
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.openbitfun.mobile.core.protocol

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

class WorkspaceHostTest {
@Test fun retainsHostAndAcceptsLegacyPayload() {
val json = Json { ignoreUnknownKeys = true }
val legacy = json.decodeFromString<RecentWorkspaceEntryResponse>("""{"path":"/app","name":"App"}""")
assertNull(legacy.remoteSshHost)
for (host in listOf("10.0.0.8", "build.example.org", "2001:db8::1")) {
val entry = json.decodeFromString<RecentWorkspaceEntryResponse>("""{"path":"/app","name":"App","remote_ssh_host":"$host"}""")
assertEquals(host, entry.remoteSshHost)
assertEquals(entry, json.decodeFromString<RecentWorkspaceEntryResponse>(json.encodeToString(entry)))
}
}
}