Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f235fb8
docs: add sidebar navigation rework design spec
ratulsarna Mar 15, 2026
7fd6932
docs: fix spec review issues in sidebar navigation design
ratulsarna Mar 15, 2026
bcc8a4f
docs: add sidebar navigation rework implementation plan
ratulsarna Mar 15, 2026
5243dfd
chore: add .worktrees/ to .gitignore
ratulsarna Mar 15, 2026
9ab63d6
feat: add SidebarViewModel with workspace + session management
ratulsarna Mar 15, 2026
37d203c
feat: wire SidebarViewModel into DI and iOS bridge
ratulsarna Mar 15, 2026
40f8d3a
fix: address code quality issues in SidebarViewModel
ratulsarna Mar 15, 2026
234bc28
fix: show cached sessions immediately, refresh in background
ratulsarna Mar 15, 2026
c090008
feat: add WorkspaceCardView with glass styling and expand/collapse
ratulsarna Mar 15, 2026
75cd4eb
feat: add WorkspacesSidebarView with expandable workspace cards
ratulsarna Mar 15, 2026
02c745c
feat: replace sessions button with hamburger, add session title + wor…
ratulsarna Mar 15, 2026
7dcaa41
feat: replace NavigationStack with NavigationSplitView, wire sidebar
ratulsarna Mar 15, 2026
0e39642
refactor: remove workspace and sessions navigation from Settings
ratulsarna Mar 15, 2026
c2a7822
refactor: delete old Sessions/Workspaces views, ViewModels, and DI wi…
ratulsarna Mar 15, 2026
fc79091
fix: use inline glassEffect calls instead of GlassEffect type variable
ratulsarna Mar 15, 2026
b3bf8b1
fix: redesign sidebar UI to match reference screenshots
ratulsarna Mar 15, 2026
94f8d38
fix: use Color.accentColor instead of .accentColor for ShapeStyle
ratulsarna Mar 15, 2026
173220b
fix: use dismiss() to navigate back to chat on session tap
ratulsarna Mar 15, 2026
4ca5bbc
fix: replace NavigationSplitView with NavigationStack push for sidebar
ratulsarna Mar 15, 2026
e2cd994
feat: enhance session retrieval with directory filtering
ratulsarna Mar 15, 2026
13e88f4
refactor: remove sidebar navigation rework implementation and design …
ratulsarna Mar 15, 2026
60c2b12
feat: enhance workspace creation error handling in SidebarViewModel
ratulsarna Mar 15, 2026
37b865d
fix: update DEVELOPMENT_TEAM to use dynamic TEAM_ID variable
ratulsarna Mar 15, 2026
0c54e0d
fix: address sidebar review feedback
ratulsarna Mar 15, 2026
24fa7c9
fix: harden sidebar error handling
ratulsarna Mar 15, 2026
8d185ed
feat: improve session loading and switching functionality in SidebarV…
ratulsarna Mar 16, 2026
5de662d
fix: keep sidebar session state in sync
ratulsarna Mar 16, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ companion/oc-pocket/bin/

# oc-pocket release/build artifacts
companion/oc-pocket/dist/

# Git worktrees
.worktrees/
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import com.ratulsarna.ocmobile.ui.screen.chat.ChatViewModel
import com.ratulsarna.ocmobile.ui.screen.connect.ConnectViewModel
import com.ratulsarna.ocmobile.ui.screen.docs.MarkdownFileViewerViewModel
import com.ratulsarna.ocmobile.ui.screen.filebrowser.FileBrowserViewModel
import com.ratulsarna.ocmobile.ui.screen.sessions.SessionsViewModel
import com.ratulsarna.ocmobile.ui.screen.settings.SettingsViewModel
import com.ratulsarna.ocmobile.ui.screen.workspaces.WorkspacesViewModel
import com.ratulsarna.ocmobile.ui.screen.sidebar.SidebarViewModel

/**
* iOS-facing ViewModel owners for SwiftUI.
Expand Down Expand Up @@ -44,8 +43,8 @@ class IosAppViewModelOwner : ViewModelStoreOwner {
/** Matches Compose `viewModel(key = "settings") { AppModule.createSettingsViewModel() }` */
fun settingsViewModel(): SettingsViewModel = get(key = "settings") { AppModule.createSettingsViewModel() }

/** Matches Compose `viewModel(key = "workspaces") { AppModule.createWorkspacesViewModel() }` */
fun workspacesViewModel(): WorkspacesViewModel = get(key = "workspaces") { AppModule.createWorkspacesViewModel() }
/** App-scoped sidebar combining workspaces + sessions. */
fun sidebarViewModel(): SidebarViewModel = get(key = "sidebar") { AppModule.createSidebarViewModel() }

/** App-scoped: used for onboarding/pairing flow. */
fun connectViewModel(): ConnectViewModel = get(key = "connect") { AppModule.createConnectViewModel() }
Expand Down Expand Up @@ -86,8 +85,6 @@ class IosScreenViewModelOwner : ViewModelStoreOwner {
viewModelStore.clear()
}

fun sessionsViewModel(): SessionsViewModel = get(key = "sessions") { AppModule.createSessionsViewModel() }

/** Matches Compose `viewModel(key = "markdown-$path-$openId") { AppModule.createMarkdownFileViewerViewModel(path) }` */
fun markdownFileViewerViewModel(path: String, openId: Long): MarkdownFileViewerViewModel =
get(key = "markdown-$path-$openId") { AppModule.createMarkdownFileViewerViewModel(path) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ interface OpenCodeApi {
suspend fun getSessions(
search: String? = null,
limit: Int? = null,
start: Long? = null
start: Long? = null,
directory: String? = null
): List<SessionDto>

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,13 @@ class OpenCodeApiImpl(
return httpClient.get("$baseUrl/session/$sessionId").body()
}

override suspend fun getSessions(search: String?, limit: Int?, start: Long?): List<SessionDto> {
override suspend fun getSessions(search: String?, limit: Int?, start: Long?, directory: String?): List<SessionDto> {
return httpClient.get("$baseUrl/session") {
val dir = directory?.trim()?.takeIf { it.isNotBlank() }
if (dir != null) {
headers.remove("x-opencode-directory")
header("x-opencode-directory", dir)
}
if (start != null) parameter("start", start)
if (!search.isNullOrBlank()) parameter("search", search)
if (limit != null) parameter("limit", limit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ class MockOpenCodeApi(
override suspend fun getSession(sessionId: String): SessionDto =
state.getSession(sessionId) ?: throw RuntimeException("Session not found: $sessionId")

override suspend fun getSessions(search: String?, limit: Int?, start: Long?): List<SessionDto> =
state.getSessions(search = search, limit = limit, start = start)
override suspend fun getSessions(search: String?, limit: Int?, start: Long?, directory: String?): List<SessionDto> =
state.getSessions(search = search, limit = limit, start = start, directory = directory)

override suspend fun createSession(request: CreateSessionRequest): SessionDto {
return state.createSession(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,12 @@ class MockState {
sessions[id]
}

suspend fun getSessions(search: String?, limit: Int?, start: Long?): List<SessionDto> = mutex.withLock {
suspend fun getSessions(search: String?, limit: Int?, start: Long?, directory: String?): List<SessionDto> = mutex.withLock {
val filtered = sessions.values.asSequence()
.filter { dto ->
if (directory.isNullOrBlank()) return@filter true
dto.directory == directory
}
.filter { dto ->
if (start == null) return@filter true
dto.time.updated >= start
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ class SessionRepositoryImpl(
}
}

override suspend fun getSessions(search: String?, limit: Int?, start: Long?): Result<List<Session>> {
override suspend fun getSessions(search: String?, limit: Int?, start: Long?, directory: String?): Result<List<Session>> {
return runCatching {
api.getSessions(search = search, limit = limit, start = start).map { it.toDomain() }
api.getSessions(search = search, limit = limit, start = start, directory = directory).map { it.toDomain() }
}.recoverCatching { e ->
when (e) {
is ClientRequestException -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ import com.ratulsarna.ocmobile.ui.screen.chat.ChatViewModel
import com.ratulsarna.ocmobile.ui.screen.docs.MarkdownFileViewerViewModel
import com.ratulsarna.ocmobile.ui.screen.filebrowser.FileBrowserViewModel
import com.ratulsarna.ocmobile.ui.screen.connect.ConnectViewModel
import com.ratulsarna.ocmobile.ui.screen.sessions.SessionsViewModel
import com.ratulsarna.ocmobile.ui.screen.settings.SettingsViewModel
import com.ratulsarna.ocmobile.ui.screen.workspaces.WorkspacesViewModel
import com.ratulsarna.ocmobile.ui.screen.sidebar.SidebarViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -260,8 +259,9 @@ object AppModule {
)
}

fun createSessionsViewModel(): SessionsViewModel {
return SessionsViewModel(
fun createSidebarViewModel(): SidebarViewModel {
return SidebarViewModel(
workspaceRepository = graphWorkspaceRepository(),
sessionRepository = graphSessionRepository(),
appSettings = appSettings
)
Expand All @@ -286,10 +286,6 @@ object AppModule {
)
}

fun createWorkspacesViewModel(): WorkspacesViewModel {
return WorkspacesViewModel(workspaceRepository = graphWorkspaceRepository())
}

fun createMarkdownFileViewerViewModel(path: String): MarkdownFileViewerViewModel {
return MarkdownFileViewerViewModel(path, graphVaultRepository())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ interface SessionRepository {
suspend fun getSessions(
search: String? = null,
limit: Int? = null,
start: Long? = null
start: Long? = null,
directory: String? = null
): Result<List<Session>>

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.ratulsarna.ocmobile.domain.model.VaultEntry
*/
data class ChatUiState(
val currentSessionId: String? = null,
val currentSessionTitle: String? = null,
/** If set, the session is in "reverted" state and messages after this point should be hidden. */
val revertMessageId: String? = null,
val messages: List<Message> = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ class ChatViewModel(
)
}

private fun normalizedSessionTitle(title: String?): String? = title?.takeIf(String::isNotBlank)

/** Current selected agent (from persistent storage) */
private var currentAgent: String? = null

Expand Down Expand Up @@ -1156,6 +1158,7 @@ class ChatViewModel(
_uiState.update {
it.copy(
currentSessionId = null,
currentSessionTitle = null,
revertMessageId = null,
messages = emptyList(),
lastGoodMessageId = null,
Expand All @@ -1176,6 +1179,7 @@ class ChatViewModel(
_uiState.update {
it.copy(
currentSessionId = newSessionId,
currentSessionTitle = null,
revertMessageId = null,
messages = emptyList(),
lastGoodMessageId = null,
Expand All @@ -1190,7 +1194,11 @@ class ChatViewModel(
val session = sessionRepository.getSession(newSessionId).getOrNull()
if (newSessionId == _uiState.value.currentSessionId) {
val revertMessageId = session?.revert?.messageId
_uiState.update { state -> applyRevertPointer(state, revertMessageId) }
_uiState.update { state ->
applyRevertPointer(state, revertMessageId).copy(
currentSessionTitle = normalizedSessionTitle(session?.title)
)
}
viewModelScope.launch {
contextUsageRepository.updateUsage(_uiState.value.messages)
}
Expand Down Expand Up @@ -1233,15 +1241,25 @@ class ChatViewModel(
}

OcMobileLog.d(TAG, "loadCurrentSession: REST returned sessionId=$sessionId")
_uiState.update { it.copy(currentSessionId = sessionId, revertMessageId = null) }
_uiState.update {
it.copy(
currentSessionId = sessionId,
currentSessionTitle = null,
revertMessageId = null
)
}
refreshPendingPermissions()

// Resolve revert pointer before loading messages to avoid transiently rendering hidden messages,
// and to keep derived state consistent.
val session = sessionRepository.getSession(sessionId).getOrNull()
if (sessionId == _uiState.value.currentSessionId) {
val revertMessageId = session?.revert?.messageId
_uiState.update { state -> applyRevertPointer(state, revertMessageId) }
_uiState.update { state ->
applyRevertPointer(state, revertMessageId).copy(
currentSessionTitle = normalizedSessionTitle(session?.title)
)
}
viewModelScope.launch {
contextUsageRepository.updateUsage(_uiState.value.messages)
}
Expand Down Expand Up @@ -1767,7 +1785,11 @@ class ChatViewModel(

val revertMessageId = event.session.revert?.messageId
val previousRevertMessageId = _uiState.value.revertMessageId
_uiState.update { state -> applyRevertPointer(state, revertMessageId) }
_uiState.update { state ->
applyRevertPointer(state, revertMessageId).copy(
currentSessionTitle = normalizedSessionTitle(event.session.title)
)
}
viewModelScope.launch {
contextUsageRepository.updateUsage(_uiState.value.messages)
}
Expand Down Expand Up @@ -2102,6 +2124,7 @@ class ChatViewModel(
lastGoodMessageId = if (response.error == null) response.id else it.lastGoodMessageId,
// Update local state to actual session (handles session cycling)
currentSessionId = response.sessionId,
currentSessionTitle = if (response.sessionId == it.currentSessionId) it.currentSessionTitle else null,
// After the user resumes, OpenCode cleans up reverted messages and clears the pointer.
revertMessageId = null
)
Expand Down Expand Up @@ -2399,6 +2422,7 @@ class ChatViewModel(
_uiState.update {
it.copy(
currentSessionId = newSession.id,
currentSessionTitle = normalizedSessionTitle(newSession.title),
revertMessageId = null,
messages = emptyList(),
lastGoodMessageId = null,
Expand Down Expand Up @@ -2576,6 +2600,7 @@ class ChatViewModel(
_uiState.update {
it.copy(
currentSessionId = sessionId,
currentSessionTitle = null,
revertMessageId = null,
messages = emptyList(), // Clear while loading
isLoading = true,
Expand All @@ -2589,7 +2614,11 @@ class ChatViewModel(
val session = sessionRepository.getSession(sessionId).getOrNull()
if (sessionId == _uiState.value.currentSessionId) {
val revertMessageId = session?.revert?.messageId
_uiState.update { state -> applyRevertPointer(state, revertMessageId) }
_uiState.update { state ->
applyRevertPointer(state, revertMessageId).copy(
currentSessionTitle = normalizedSessionTitle(session?.title)
)
}
viewModelScope.launch {
contextUsageRepository.updateUsage(_uiState.value.messages)
}
Expand Down
Loading
Loading