diff --git a/ui/desktop/src/__tests__/sessions.test.ts b/ui/desktop/src/__tests__/sessions.test.ts
index 97e7daca2..e865e2019 100644
--- a/ui/desktop/src/__tests__/sessions.test.ts
+++ b/ui/desktop/src/__tests__/sessions.test.ts
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
-import { shouldShowNewChatTitle } from '../sessions';
-import { getSessionDisplayName, prependUnique } from '../hooks/useNavigationSessions';
+import { getSessionDisplayName, shouldShowNewChatTitle } from '../sessions';
+import { prependUnique } from '../hooks/useNavigationSessions';
import type { Session } from '../api';
import type { SessionListItem } from '../acp/sessions';
diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx
index d691dad9e..dd722175f 100644
--- a/ui/desktop/src/components/BaseChat.tsx
+++ b/ui/desktop/src/components/BaseChat.tsx
@@ -39,6 +39,7 @@ import { Recipe } from '../recipe';
import { useAutoSubmit } from '../hooks/useAutoSubmit';
import { Goose } from './icons';
import EnvironmentBadge from './GooseSidebar/EnvironmentBadge';
+import SessionActionsHeader from './SessionActionsHeader';
const i18n = defineMessages({
failedToLoadSession: {
@@ -99,7 +100,7 @@ export default function BaseChat({
const navContext = useNavigationContextSafe();
const setView = useNavigation();
const isNavCollapsed = !navContext?.isNavExpanded;
- const contentClassName = cn('pr-1 pb-10 pt-10', (isMobile || isNavCollapsed) && 'pt-14');
+ const contentClassName = cn('pr-1 pb-10 pt-12', (isMobile || isNavCollapsed) && 'pt-16');
const { droppedFiles, setDroppedFiles, handleDrop, handleDragOver } = useFileDrop();
const onStreamFinish = useCallback(() => {}, []);
const [isCreateRecipeModalOpen, setIsCreateRecipeModalOpen] = useState(false);
@@ -406,7 +407,7 @@ export default function BaseChat({
{/* Chat container with sticky recipe header */}
{/* Goose watermark - top right */}
-
+
+
+
{
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function getNodePath(parentPath: string, key: string, isArrayItem: boolean): string {
+ if (isArrayItem) {
+ return `${parentPath}[${key}]`;
+ }
+
+ return /^[A-Za-z_$][\w$]*$/.test(key)
+ ? `${parentPath}.${key}`
+ : `${parentPath}[${JSON.stringify(key)}]`;
+}
+
+function getStringPreview(value: string): string {
+ if (value.length <= LONG_STRING_THRESHOLD) {
+ return JSON.stringify(value);
+ }
+
+ return JSON.stringify(
+ `${value.slice(0, STRING_PREVIEW_START)} ... ${value.slice(-STRING_PREVIEW_END)}`
+ );
+}
+
+function JsonPrimitiveValue({
+ value,
+ path,
+ onOpenText,
+}: {
+ value: unknown;
+ path: string;
+ onOpenText: (selection: FullTextSelection) => void;
+}) {
+ if (typeof value === 'string') {
+ const isLong = value.length > LONG_STRING_THRESHOLD;
+ const preview = getStringPreview(value);
+
+ if (isLong) {
+ return (
+
+ );
+ }
+
+ return (
+ {preview}
+ );
+ }
+
+ if (typeof value === 'number') {
+ return {value};
+ }
+
+ if (typeof value === 'boolean') {
+ return {String(value)};
+ }
+
+ if (value === null) {
+ return null;
+ }
+
+ return {String(value)};
+}
+
+function JsonTreeNode({
+ label,
+ value,
+ depth,
+ path,
+ isArrayItem = false,
+ onOpenText,
+}: {
+ label?: string;
+ value: unknown;
+ depth: number;
+ path: string;
+ isArrayItem?: boolean;
+ onOpenText: (selection: FullTextSelection) => void;
+}) {
+ const isArray = Array.isArray(value);
+ const isRecord = isJsonRecord(value);
+ const isContainer = isArray || isRecord;
+ const [isOpen, setIsOpen] = useState(depth < 3);
+
+ const labelNode =
+ label === undefined ? null : (
+ {isArrayItem ? label : JSON.stringify(label)}:
+ );
+
+ if (!isContainer) {
+ return (
+
+ {labelNode}
+
+
+ );
+ }
+
+ const entries = isArray
+ ? value.map((item, index) => [String(index), item] as const)
+ : Object.entries(value);
+ const openToken = isArray ? '[' : '{';
+ const closeToken = isArray ? ']' : '}';
+ const countLabel = isArray
+ ? `${entries.length} ${entries.length === 1 ? 'item' : 'items'}`
+ : `${entries.length} ${entries.length === 1 ? 'key' : 'keys'}`;
+
+ return (
+
+
+
+ {isOpen && entries.length > 0 && (
+
+ {entries.map(([key, childValue]) => (
+
+ ))}
+
{closeToken}
+
+ )}
+
+ );
+}
+
+function JsonTree({
+ value,
+ onOpenText,
+}: {
+ value: unknown;
+ onOpenText: (selection: FullTextSelection) => void;
+}) {
+ return (
+
+
+
+ );
+}
+
+export default function SessionActionsHeader({ session, className }: SessionActionsHeaderProps) {
+ const intl = useIntl();
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
+ const [jsonValue, setJsonValue] = useState(null);
+ const [jsonText, setJsonText] = useState('');
+ const [isJsonLoading, setIsJsonLoading] = useState(false);
+ const [fullTextSelection, setFullTextSelection] = useState(null);
+
+ const title = useMemo(() => (session ? getSessionDisplayName(session) : ''), [session]);
+
+ const handleViewJson = useCallback(async () => {
+ if (!session) return;
+
+ setIsJsonOpen(true);
+ setJsonValue(null);
+ setJsonText('');
+ setIsJsonLoading(true);
+ try {
+ const json = await acpExportSession(session.id);
+ const parsed = parseSessionJson(json);
+ setJsonValue(parsed.value);
+ setJsonText(parsed.pretty);
+ } catch (error) {
+ setIsJsonOpen(false);
+ toast.error(
+ intl.formatMessage(i18n.jsonFailed, {
+ error: errorMessage(error, 'Unknown error'),
+ })
+ );
+ } finally {
+ setIsJsonLoading(false);
+ }
+ }, [intl, session]);
+
+ const handleCopyJson = useCallback(async () => {
+ if (!jsonText) return;
+ await navigator.clipboard.writeText(jsonText);
+ toast.success(intl.formatMessage(i18n.copiedJson));
+ }, [intl, jsonText]);
+
+ const handleCopyFullText = useCallback(async () => {
+ if (!fullTextSelection) return;
+ await navigator.clipboard.writeText(fullTextSelection.value);
+ toast.success(intl.formatMessage(i18n.copiedText));
+ }, [fullTextSelection, intl]);
+
+ const handleJsonOpenChange = useCallback((open: boolean) => {
+ setIsJsonOpen(open);
+ if (!open) {
+ setFullTextSelection(null);
+ }
+ }, []);
+
+ if (!session) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/ui/desktop/src/hooks/useNavigationSessions.ts b/ui/desktop/src/hooks/useNavigationSessions.ts
index 511339a8d..6977cefa8 100644
--- a/ui/desktop/src/hooks/useNavigationSessions.ts
+++ b/ui/desktop/src/hooks/useNavigationSessions.ts
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import { getSession } from '../api';
import { useChatContext } from '../contexts/ChatContext';
-import { shouldShowNewChatTitle } from '../sessions';
+import { getSessionDisplayName } from '../sessions';
import { AppEvents } from '../constants/events';
import type { Session } from '../api';
import { acpListRecentSessions, type SessionListItem } from '../acp/sessions';
@@ -200,16 +200,3 @@ export function useNavigationSessions() {
handleSessionClick,
};
}
-
-export function getSessionDisplayName(session: Session): string {
- if (session.user_set_name) {
- return session.name;
- }
- if (session.recipe?.title) {
- return session.recipe.title;
- }
- if (shouldShowNewChatTitle(session)) {
- return 'New Chat';
- }
- return session.name;
-}
diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json
index 0b85605f7..c1df31f1e 100644
--- a/ui/desktop/src/i18n/messages/en.json
+++ b/ui/desktop/src/i18n/messages/en.json
@@ -3746,6 +3746,36 @@
"securityToggle.warpNotice": {
"defaultMessage": "Command injection detection works best when connected to WARP (required to reach the classification service)."
},
+ "sessionActionsHeader.close": {
+ "defaultMessage": "Close"
+ },
+ "sessionActionsHeader.copiedJson": {
+ "defaultMessage": "Session JSON copied"
+ },
+ "sessionActionsHeader.copiedText": {
+ "defaultMessage": "Text copied"
+ },
+ "sessionActionsHeader.copyJson": {
+ "defaultMessage": "Copy JSON"
+ },
+ "sessionActionsHeader.copyText": {
+ "defaultMessage": "Copy text"
+ },
+ "sessionActionsHeader.fullTextTitle": {
+ "defaultMessage": "Text value"
+ },
+ "sessionActionsHeader.jsonFailed": {
+ "defaultMessage": "Failed to load session JSON: {error}"
+ },
+ "sessionActionsHeader.jsonTitle": {
+ "defaultMessage": "Session JSON"
+ },
+ "sessionActionsHeader.loadingJson": {
+ "defaultMessage": "Loading JSON..."
+ },
+ "sessionActionsHeader.viewJson": {
+ "defaultMessage": "View session JSON"
+ },
"sessionHistory.cancel": {
"defaultMessage": "Cancel"
},
diff --git a/ui/desktop/src/i18n/messages/hi.json b/ui/desktop/src/i18n/messages/hi.json
index 76c9a5903..e91496b58 100644
--- a/ui/desktop/src/i18n/messages/hi.json
+++ b/ui/desktop/src/i18n/messages/hi.json
@@ -3746,6 +3746,36 @@
"securityToggle.warpNotice": {
"defaultMessage": "WARP से कनेक्ट होने पर कमांड इंजेक्शन डिटेक्शन सबसे अच्छा काम करता है (वर्गीकरण सेवा तक पहुंचने के लिए आवश्यक)।"
},
+ "sessionActionsHeader.close": {
+ "defaultMessage": "Close"
+ },
+ "sessionActionsHeader.copiedJson": {
+ "defaultMessage": "Session JSON copied"
+ },
+ "sessionActionsHeader.copiedText": {
+ "defaultMessage": "Text copied"
+ },
+ "sessionActionsHeader.copyJson": {
+ "defaultMessage": "Copy JSON"
+ },
+ "sessionActionsHeader.copyText": {
+ "defaultMessage": "Copy text"
+ },
+ "sessionActionsHeader.fullTextTitle": {
+ "defaultMessage": "Text value"
+ },
+ "sessionActionsHeader.jsonFailed": {
+ "defaultMessage": "Failed to load session JSON: {error}"
+ },
+ "sessionActionsHeader.jsonTitle": {
+ "defaultMessage": "Session JSON"
+ },
+ "sessionActionsHeader.loadingJson": {
+ "defaultMessage": "Loading JSON..."
+ },
+ "sessionActionsHeader.viewJson": {
+ "defaultMessage": "View session JSON"
+ },
"sessionHistory.cancel": {
"defaultMessage": "रद्द करें"
},
diff --git a/ui/desktop/src/i18n/messages/ja.json b/ui/desktop/src/i18n/messages/ja.json
index f18fe9ed0..9b68c706b 100644
--- a/ui/desktop/src/i18n/messages/ja.json
+++ b/ui/desktop/src/i18n/messages/ja.json
@@ -3746,6 +3746,36 @@
"securityToggle.warpNotice": {
"defaultMessage": "コマンドインジェクション検出は、WARPに接続していると最も効果的です(分類サービスに到達するために必要です)。"
},
+ "sessionActionsHeader.close": {
+ "defaultMessage": "Close"
+ },
+ "sessionActionsHeader.copiedJson": {
+ "defaultMessage": "Session JSON copied"
+ },
+ "sessionActionsHeader.copiedText": {
+ "defaultMessage": "Text copied"
+ },
+ "sessionActionsHeader.copyJson": {
+ "defaultMessage": "Copy JSON"
+ },
+ "sessionActionsHeader.copyText": {
+ "defaultMessage": "Copy text"
+ },
+ "sessionActionsHeader.fullTextTitle": {
+ "defaultMessage": "Text value"
+ },
+ "sessionActionsHeader.jsonFailed": {
+ "defaultMessage": "Failed to load session JSON: {error}"
+ },
+ "sessionActionsHeader.jsonTitle": {
+ "defaultMessage": "Session JSON"
+ },
+ "sessionActionsHeader.loadingJson": {
+ "defaultMessage": "Loading JSON..."
+ },
+ "sessionActionsHeader.viewJson": {
+ "defaultMessage": "View session JSON"
+ },
"sessionHistory.cancel": {
"defaultMessage": "キャンセル"
},
diff --git a/ui/desktop/src/i18n/messages/ru.json b/ui/desktop/src/i18n/messages/ru.json
index cd7b1de8b..3eadae7aa 100644
--- a/ui/desktop/src/i18n/messages/ru.json
+++ b/ui/desktop/src/i18n/messages/ru.json
@@ -3746,6 +3746,36 @@
"securityToggle.warpNotice": {
"defaultMessage": "Обнаружение внедрения команд работает лучше всего при подключении к WARP (требуется для доступа к сервису классификации)."
},
+ "sessionActionsHeader.close": {
+ "defaultMessage": "Close"
+ },
+ "sessionActionsHeader.copiedJson": {
+ "defaultMessage": "Session JSON copied"
+ },
+ "sessionActionsHeader.copiedText": {
+ "defaultMessage": "Text copied"
+ },
+ "sessionActionsHeader.copyJson": {
+ "defaultMessage": "Copy JSON"
+ },
+ "sessionActionsHeader.copyText": {
+ "defaultMessage": "Copy text"
+ },
+ "sessionActionsHeader.fullTextTitle": {
+ "defaultMessage": "Text value"
+ },
+ "sessionActionsHeader.jsonFailed": {
+ "defaultMessage": "Failed to load session JSON: {error}"
+ },
+ "sessionActionsHeader.jsonTitle": {
+ "defaultMessage": "Session JSON"
+ },
+ "sessionActionsHeader.loadingJson": {
+ "defaultMessage": "Loading JSON..."
+ },
+ "sessionActionsHeader.viewJson": {
+ "defaultMessage": "View session JSON"
+ },
"sessionHistory.cancel": {
"defaultMessage": "Отмена"
},
diff --git a/ui/desktop/src/i18n/messages/tr.json b/ui/desktop/src/i18n/messages/tr.json
index c5838ba2f..fbaacc35e 100644
--- a/ui/desktop/src/i18n/messages/tr.json
+++ b/ui/desktop/src/i18n/messages/tr.json
@@ -3746,6 +3746,36 @@
"securityToggle.warpNotice": {
"defaultMessage": "Komut enjeksiyonu algılama, WARP bağlantısı olduğunda en iyi şekilde çalışır (sınıflandırma hizmetine erişmek için gereklidir)."
},
+ "sessionActionsHeader.close": {
+ "defaultMessage": "Close"
+ },
+ "sessionActionsHeader.copiedJson": {
+ "defaultMessage": "Session JSON copied"
+ },
+ "sessionActionsHeader.copiedText": {
+ "defaultMessage": "Text copied"
+ },
+ "sessionActionsHeader.copyJson": {
+ "defaultMessage": "Copy JSON"
+ },
+ "sessionActionsHeader.copyText": {
+ "defaultMessage": "Copy text"
+ },
+ "sessionActionsHeader.fullTextTitle": {
+ "defaultMessage": "Text value"
+ },
+ "sessionActionsHeader.jsonFailed": {
+ "defaultMessage": "Failed to load session JSON: {error}"
+ },
+ "sessionActionsHeader.jsonTitle": {
+ "defaultMessage": "Session JSON"
+ },
+ "sessionActionsHeader.loadingJson": {
+ "defaultMessage": "Loading JSON..."
+ },
+ "sessionActionsHeader.viewJson": {
+ "defaultMessage": "View session JSON"
+ },
"sessionHistory.cancel": {
"defaultMessage": "İptal"
},
diff --git a/ui/desktop/src/i18n/messages/zh-CN.json b/ui/desktop/src/i18n/messages/zh-CN.json
index 1c4389454..5dc9bff78 100644
--- a/ui/desktop/src/i18n/messages/zh-CN.json
+++ b/ui/desktop/src/i18n/messages/zh-CN.json
@@ -3746,6 +3746,36 @@
"securityToggle.warpNotice": {
"defaultMessage": "连接到 WARP 时,命令注入检测效果最佳(访问分类服务需要 WARP)。"
},
+ "sessionActionsHeader.close": {
+ "defaultMessage": "Close"
+ },
+ "sessionActionsHeader.copiedJson": {
+ "defaultMessage": "Session JSON copied"
+ },
+ "sessionActionsHeader.copiedText": {
+ "defaultMessage": "Text copied"
+ },
+ "sessionActionsHeader.copyJson": {
+ "defaultMessage": "Copy JSON"
+ },
+ "sessionActionsHeader.copyText": {
+ "defaultMessage": "Copy text"
+ },
+ "sessionActionsHeader.fullTextTitle": {
+ "defaultMessage": "Text value"
+ },
+ "sessionActionsHeader.jsonFailed": {
+ "defaultMessage": "Failed to load session JSON: {error}"
+ },
+ "sessionActionsHeader.jsonTitle": {
+ "defaultMessage": "Session JSON"
+ },
+ "sessionActionsHeader.loadingJson": {
+ "defaultMessage": "Loading JSON..."
+ },
+ "sessionActionsHeader.viewJson": {
+ "defaultMessage": "View session JSON"
+ },
"sessionHistory.cancel": {
"defaultMessage": "取消"
},
diff --git a/ui/desktop/src/sessions.ts b/ui/desktop/src/sessions.ts
index ac787bc6b..33cfd1bf6 100644
--- a/ui/desktop/src/sessions.ts
+++ b/ui/desktop/src/sessions.ts
@@ -1,4 +1,5 @@
import { Session, startAgent, ExtensionConfig } from './api';
+import { DEFAULT_CHAT_TITLE } from './contexts/ChatContext';
import type { setViewType } from './hooks/useNavigation';
import {
getExtensionConfigsWithOverrides,
@@ -16,6 +17,19 @@ export function shouldShowNewChatTitle(session: Session): boolean {
return !session.user_set_name && session.message_count === 0;
}
+export function getSessionDisplayName(session: Session): string {
+ if (session.user_set_name) {
+ return session.name;
+ }
+ if (session.recipe?.title) {
+ return session.recipe.title;
+ }
+ if (shouldShowNewChatTitle(session)) {
+ return DEFAULT_CHAT_TITLE;
+ }
+ return session.name;
+}
+
export function resumeSession(session: Session, setView: setViewType) {
const eventDetail = {
sessionId: session.id,