From 9804a8c782509dd2c39fe43aaf3b06033542f10a Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Tue, 16 Jun 2026 22:30:09 -0400 Subject: [PATCH] View json (#9678) Signed-off-by: Douwe M Osinga Co-authored-by: Douwe M Osinga --- ui/desktop/src/__tests__/sessions.test.ts | 4 +- ui/desktop/src/components/BaseChat.tsx | 7 +- .../src/components/SessionActionsHeader.tsx | 395 ++++++++++++++++++ ui/desktop/src/hooks/useNavigationSessions.ts | 15 +- ui/desktop/src/i18n/messages/en.json | 30 ++ ui/desktop/src/i18n/messages/hi.json | 30 ++ ui/desktop/src/i18n/messages/ja.json | 30 ++ ui/desktop/src/i18n/messages/ru.json | 30 ++ ui/desktop/src/i18n/messages/tr.json | 30 ++ ui/desktop/src/i18n/messages/zh-CN.json | 30 ++ ui/desktop/src/sessions.ts | 14 + 11 files changed, 597 insertions(+), 18 deletions(-) create mode 100644 ui/desktop/src/components/SessionActionsHeader.tsx 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 ( + <> +
+ +
+ + + + + {intl.formatMessage(i18n.jsonTitle)} + +
+ {isJsonLoading ? ( +
+ + {intl.formatMessage(i18n.loadingJson)} +
+ ) : ( +
+ +
+ )} +
+ + + + +
+
+ + !open && setFullTextSelection(null)} + > + + + {intl.formatMessage(i18n.fullTextTitle)} + + {fullTextSelection && ( +
+ + {fullTextSelection.path} + +
+                {fullTextSelection.value}
+              
+
+ )} + + + + +
+
+ + ); +} 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,