openhands / src /api /conversation-metadata-store.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
63522a5 verified
Raw
History Blame Contribute Delete
4.68 kB
import { Provider } from "#/types/settings";
import type { PluginSpec } from "#/api/conversation-service/agent-server-conversation-service.types";
const STORAGE_KEY = "openhands-agent-server-conversation-metadata";
export type WorkspaceMode = "local_repo" | "new_worktree";
export interface ConversationMetadata {
selected_repository: string | null;
selected_branch: string | null;
git_provider: Provider | null;
/**
* The local workspace path the user explicitly attached at conversation
* creation time. Distinct from `selected_repository` (which is set by
* the repo picker on the home page). Used by the Files tab to decide
* whether to default to diff view: if the user attached *anything*
* (repo or local workspace), we lean diff-first because there's a real
* git baseline to compare against.
*/
selected_workspace?: string | null;
/**
* How the conversation should use `selected_workspace`.
*
* `local_repo` means the runtime should operate directly in the selected
* folder, even when it is not a git checkout. `new_worktree` preserves the
* historical agent-server behavior for conversations that should start in a
* generated per-conversation worktree.
*/
workspace_mode?: WorkspaceMode | null;
/**
* The LLM profile the conversation was created with (or last switched to).
* Client-side only. Lets the chat-header switcher show the exact profile
* name even when several profiles share the same underlying model — the
* agent-server only round-trips the model string, so matching on it alone
* is ambiguous (issue #1082).
*/
active_profile?: string | null;
/**
* When `active_profile` was stamped (ISO string). Written by the live
* WebSocket handler (the observation's server timestamp) and the user
* `/model` mutation (client clock). The history seed compares the latest
* SwitchLLM observation's timestamp against this so a reload can repair a
* missed agent switch without rolling back a newer manual switch — manual
* `/model` switches go through REST and leave no observation in history.
* Absent on stamps written before this field existed; treated as older
* than any observation so the seed can still repair them.
*/
stamped_at?: string | null;
/** Store plugin coordinates only; parameters may contain secrets. */
plugins?: PluginSpec[] | null;
/**
* Local-only client-managed planning conversation id. Cloud backends expose
* real sub-conversations through `sub_conversation_ids`; local agent-server
* backends do not, so Canvas stores the relationship here.
*/
local_planning_conversation_id?: string | null;
}
export const toPluginCoordinates = (plugin: PluginSpec): PluginSpec => ({
source: plugin.source,
ref: plugin.ref ?? null,
repo_path: plugin.repo_path ?? null,
});
type StoredMetadata = Record<string, ConversationMetadata>;
const readAll = (): StoredMetadata => {
if (typeof window === "undefined") return {};
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== "object") return {};
return parsed as StoredMetadata;
} catch {
return {};
}
};
const writeAll = (next: StoredMetadata): void => {
if (typeof window === "undefined") return;
if (Object.keys(next).length === 0) {
window.localStorage.removeItem(STORAGE_KEY);
return;
}
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
};
export const getStoredConversationMetadata = (
conversationId: string,
): ConversationMetadata | null => readAll()[conversationId] ?? null;
export const setStoredConversationMetadata = (
conversationId: string,
metadata: ConversationMetadata,
): void => {
const all = readAll();
all[conversationId] = metadata;
writeAll(all);
};
/**
* Merge `patch` into a conversation's stored metadata, preserving every
* existing field — `setStoredConversationMetadata` does a full-object
* replace, which would otherwise silently drop fields the caller doesn't
* enumerate (e.g. `plugins`).
*/
export const mergeStoredConversationMetadata = (
conversationId: string,
patch: Partial<ConversationMetadata>,
): void => {
setStoredConversationMetadata(conversationId, {
selected_repository: null,
selected_branch: null,
git_provider: null,
...(getStoredConversationMetadata(conversationId) ?? {}),
...patch,
});
};
export const removeStoredConversationMetadata = (
conversationId: string,
): void => {
const all = readAll();
if (!(conversationId in all)) return;
delete all[conversationId];
writeAll(all);
};