wooden-umbrella-84098
08/20/2026, 8:30 PMwooden-umbrella-84098
08/20/2026, 8:31 PMwooden-umbrella-84098
08/20/2026, 8:31 PMwooden-umbrella-84098
08/20/2026, 8:32 PMwooden-umbrella-84098
08/20/2026, 8:34 PMwooden-umbrella-84098
08/20/2026, 8:34 PMstraight-dog-12443
08/20/2026, 8:49 PMwooden-umbrella-84098
08/20/2026, 8:50 PMclean-greece-29346
08/20/2026, 9:01 PMclean-greece-29346
08/20/2026, 9:02 PMclean-greece-29346
08/20/2026, 9:05 PMwooden-umbrella-84098
08/20/2026, 9:05 PMwooden-umbrella-84098
08/20/2026, 9:05 PMwooden-umbrella-84098
08/20/2026, 9:05 PMclean-greece-29346
08/20/2026, 9:09 PMclean-greece-29346
08/20/2026, 9:10 PMwooden-umbrella-84098
08/20/2026, 9:14 PMclean-greece-29346
08/20/2026, 9:17 PMclean-greece-29346
08/20/2026, 9:19 PMwooden-umbrella-84098
08/20/2026, 9:25 PMpinned-conversations in localstorage sadblobwooden-umbrella-84098
08/20/2026, 9:25 PMtags on conversations which should make this reasonably easy to fixlimited-student-10747
08/20/2026, 9:34 PMfresh-machine-37269
08/20/2026, 9:34 PMwooden-umbrella-84098
08/20/2026, 9:37 PMinvestigate the use of localstorage in this repo
localstorage should only be used for:
* the list and configuration of backends (host, password, name)
* UI configuration that shouldn't carry across devices (e.g. what panel is open)
However, we're storing things like workspace metadata for each convo ID, the list of pinned conversations, etc in localstorage. this is a huge antipattern. these things need to persist across devices.
investigate the use of localstorage and open one issue on openhands/openhands per item that needs to be migrated. in the issue mention how the backend should track it. often this will be with tags on conversations in agent-server, e.g. for pinned conversations, or with metadata already available in the backend (e.g. the workspace). for each issue, put [localstorage] in the issue title, and tag it withif you canlocalstorage
wooden-umbrella-84098
08/20/2026, 9:37 PMfresh-machine-37269
08/20/2026, 9:42 PMagent-canvas frontend + `software-agent-sdk`'s agent_server). Here's the full inventory and a concrete API design that reuses the patterns the backend already has.
What's in localStorage today Conversation-scoped state that is server truth (the real bug)
• openhands-agent-server-conversation-metadata (conversation-metadata-store.ts) → per-conversation { selected_repository, selected_branch, git_provider, selected_workspace, workspace_mode, active_profile, plugins }. This is the workspace→conversation mapping causing the "no workspace" bug.
• archived-conversations (archived-conversations-store.ts) → { [backendId]: string[] }
• pinned-conversations (pinned-conversations-store.ts) → { [backendId]: string[] }
Per-conversation UI state (device-local, but currently leaks) — conversation-state-${id} in `conversation-local-storage.ts`: selectedTab, unpinnedTabs, conversationMode, draftMessage, filesTabDiffView, filesTabContentViewMode, subConversationTaskId, plus pending-task-draft.
Global user preferences — openhands-color-theme, openhands-sidebar, conversation-panel-preferences, home-store (recent repos / last provider), openhands-automations-view, oh:home-pinned-automations, desktop-layout-panel-width, openhands-onboarded, alert_banner_dismissed_at.
Backend/connection registry — openhands-backends, openhands-active-backend, openhands-backend-health, openhands-last-conversation-by-backend.
Ephemeral / idempotency / consent — openhands-child-conversation-launches:* (tool-call dedupe ledger), draft keys, openhands-telemetry-* consent flags and PostHog keys.
The key consistency insight The backend already has three established storage mechanisms to map onto:
1. misc_settings — an opaque, frontend-owned dict persisted via PATCH /api/settings (misc_settings_diff deep-merge). Already used for misc_settings.app_preferences. The server explicitly promises to never read/validate it.
2. Conversation tags — server-persisted dict[str,str] on every conversation (ConversationInfo.tags, StoredConversation.tags). Keys are lowercase alphanumeric, values ≤256 chars. Settable at POST /api/conversations and via PATCH /api/conversations/{id} (full-map replace).
3. Typed conversation metadata — fields on ConversationInfo / UpdateConversationRequest (like title), persisted into the conversation's own JSON, edited via PATCH /api/conversations/{id}.
4. workspaces.json file store (PersistedWorkspaces / FileWorkspacesStore) — for the saved-workspace picker.
The natural split: structured, typed, per-conversation domain data → first-class fields on the conversation; boolean/flag state → tags; global UI prefs → `misc_settings`; device-local connection state + secrets → stay local.
Proposed API design 1. Promote workspace metadata to typed conversation fields (server truth) Add to the SDK's StoredConversation / _ConversationInfoBase and UpdateConversationRequest (and the wire ConversationInfo):
selected_repository: str | None = None
selected_branch: str | None = None
git_provider: str | None = None
selected_workspace: str | None = None # user-attached local dir, distinct from workspace.working_dir
workspace_mode: Literal["local_repo", "new_worktree"] | None = None
These already exist today only in the client's ConversationMetadata — lift them verbatim. They round-trip automatically through GET /api/conversations/search and are edited via the existing PATCH /api/conversations/{id} (extend UpdateConversationRequest with the same optional fields). This directly fixes the "no workspace" bug: the workspace comes from the server response, not a local map.
2. Pin/archive via conversation tags (drop the two Zustand stores) Tags are the intended home for arbitrary per-conversation flags (the thread already pointed at them). Reserve two keys and serialize the backend-scoped lists into per-conversation tags:
• pinned=1 / (absent = unpinned)
• archived=1 / (absent = not archived)
Because PATCH tags replaces the whole map (the SDK documents this footgun), add either:
• a convenience endpoint to avoid read-modify-write races:
◦ POST /api/conversations/{id}/tags/{key} (body: value) and DELETE /api/conversations/{id}/tags/{key}, or
• an explicit `mode: "merge"`/`"replace"` flag on UpdateConversationRequest.
Recommendation: POST/DELETE /api/conversations/{id}/tags/{key} — it eliminates the "drop every other tag (including acpserver)" hazard entirely and is trivially idempotent for pin/archive toggles.
3. Global UI prefs → misc_settings.ui_preferences Add a sibling ui_preferences namespace next to app_preferences (the MiscSettings container already anticipates this — the frontend comment literally says "adding a future category … is a non-breaking change"). Route through the existing PATCH /api/settings with `misc_settings_diff`:
misc_settings.ui_preferences = {
color_theme, sidebar_collapsed,
conversation_panel: { show_full_metadata, show_llm_profiles, organize_mode, sort, thread_scope, ... },
automations_view_mode, automations_view_mode,
home_pinned_automations: { [backendId]: string[] },
home_recent_repositories: [...], home_last_provider,
panel_widths: {...},
onboarded: true,
alert_banner_dismissed_at
}
Frontend keeps its current APP_PREFERENCE_FIELDS splitter approach; just widen it with a UI_PREFERENCE_FIELDS set that mirrors these keys through misc_settings_diff.ui_preferences. The server needs zero new endpoints — it's the same opaque dict path as app_preferences.
4. Per-conversation UI state → tags (or a per-conversation blob) Most of conversation-local-storage.ts is legitimately client-side (MRU tab, draft) — it doesn't need cross-device sync, and draftMessage should arguably stay ephemeral. But if you want a consistent home for the persist-worthy subset (selectedTab, filesTabDiffView, filesTabContentViewMode, conversationMode, unpinnedTabs), store them as conversation tags (selectedtab, filesdiffview, contentviewmode, conversationmode, unpinnedtabs). Keep `draftMessage`/`pending-task-draft` local (ephemeral by design).
5. Leave these in localStorage (and codify why) • openhands-backends, openhands-active-backend, openhands-backend-health, openhands-last-conversation-by-backend — backend connection state contains apiKey, is device-originated, and is not portable between machines. Moving secrets to the server would be worse.
• openhands-child-conversation-launches:* — a client-side idempotency ledger; the server has no notion of "already acted on this tool call" from the Canvas UI tool.
• openhands-telemetry-* — consent already has a server home (misc_settings.telemetry.consent); the localStorage keys are a pending-sync/PostHog cache and should stay as such.
6. One-shot migration Follow the existing app_preferences migration precedent: on first GET /api/settings (and first GET /api/conversations/search), read the legacy localStorage keys, promote them into the new server fields/tags via PATCH, then delete the localStorage key. This gives existing users a clean handoff without a fresh-install-only experience.
Recommendation summary This stays consistent with how the agent-server already stores things: typed fields for domain data, tags for flags, the opaque misc_settings blob for frontend-owned preferences, and `workspaces.json`/localStorage for device/secret-bound data. If you want, I can put this into a design doc or start the SDK-side PR (new UpdateConversationRequest fields + tags sub-resource + ui_preferences)., with interactive elementsclean-greece-29346
08/31/2026, 3:17 PMmisc_settings.ui_preferences. looking back, it makes sense. unfortunately, i was working on this at one point by referencing the issues instead of this original design doc. that took me down a different path where i assumed that we needed an update to the automations service which led to github.com/OpenHands/automation/issues/394 and github.com/OpenHands/automation/pull/395.
when i discovered this, my instinct was just to backtrack on my own but there were already comments and updates to the original issues that validated the new path that i had started heading down. I see that those comments and updates were made by an agent so it might be fine if I ignore them.
just to be safe though, @limited-student-10747 is it safe to ignore those comments here and here and backtrack to what the original design doc suggested or should i move forward with these updates to the automation service?