diff --git a/etc/servers/work-server/src/services/chat-message-parts.test.ts b/etc/servers/work-server/src/services/chat-message-parts.test.ts index b74fa22..ef01d23 100644 --- a/etc/servers/work-server/src/services/chat-message-parts.test.ts +++ b/etc/servers/work-server/src/services/chat-message-parts.test.ts @@ -14,7 +14,7 @@ test('extractChatMessageParts normalizes absolute legacy dot-codex prompt previe assert.ok(prompt); assert.equal( prompt.options[0]?.preview?.url, - '/api/chat/resources/chat-room/resource/source/chat-room-reference.md', + '/api/chat/resources/.codex_chat/chat-room/resource/source/chat-room-reference.md', ); }); @@ -32,7 +32,7 @@ test('parseChatMessageParts normalizes absolute legacy link card urls to api cha { type: 'link_card', title: 'legacy resource', - url: '/api/chat/resources/chat-room/resource/uploads/spec.png', + url: '/api/chat/resources/.codex_chat/chat-room/resource/uploads/spec.png', actionLabel: '열기', }, ]); diff --git a/etc/servers/work-server/src/services/chat-message-parts.ts b/etc/servers/work-server/src/services/chat-message-parts.ts index e151bad..5ee8bbd 100644 --- a/etc/servers/work-server/src/services/chat-message-parts.ts +++ b/etc/servers/work-server/src/services/chat-message-parts.ts @@ -90,6 +90,20 @@ const CHAT_PUBLIC_DOT_CODEX_MARKER = '/public/.codex_chat/'; const RESOURCE_MANAGER_PREVIEW_MARKER = '/api/resource-manager/preview/'; const RESOURCE_MANAGER_ROOT_MARKER = 'resource/'; +function buildCanonicalChatApiResourcePath(relativePath: string) { + const normalizedRelativePath = normalizeText(relativePath).replace(/^\/+/, ''); + + if (!normalizedRelativePath) { + return ''; + } + + if (normalizedRelativePath.startsWith('.codex_chat/')) { + return `${CHAT_API_RESOURCE_MARKER}${normalizedRelativePath}`; + } + + return `${CHAT_API_RESOURCE_MARKER}.codex_chat/${normalizedRelativePath}`; +} + function normalizeText(value: unknown) { return String(value ?? '').trim(); } @@ -239,11 +253,11 @@ function normalizeUrl(value: string) { } if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) { - return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length)}`; + return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length)); } if (pathname.startsWith(CHAT_DOT_CODEX_MARKER)) { - return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_DOT_CODEX_MARKER.length)}`; + return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_DOT_CODEX_MARKER.length)); } } catch { // Fall through to handle relative and embedded resource paths below. @@ -259,18 +273,18 @@ function normalizeUrl(value: string) { const apiPath = normalized.slice(apiMarkerIndex); const dotCodexIndex = apiPath.indexOf(CHAT_DOT_CODEX_MARKER); return dotCodexIndex >= 0 - ? `${CHAT_API_RESOURCE_MARKER}${apiPath.slice(dotCodexIndex + 1)}` + ? buildCanonicalChatApiResourcePath(apiPath.slice(dotCodexIndex + 1)) : apiPath; } const publicDotCodexIndex = normalized.lastIndexOf(CHAT_PUBLIC_DOT_CODEX_MARKER); if (publicDotCodexIndex >= 0) { - return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(publicDotCodexIndex + 8)}`; + return buildCanonicalChatApiResourcePath(normalized.slice(publicDotCodexIndex + 8)); } const dotCodexIndex = normalized.lastIndexOf(CHAT_DOT_CODEX_MARKER); if (dotCodexIndex >= 0) { - return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(dotCodexIndex + 1)}`; + return buildCanonicalChatApiResourcePath(normalized.slice(dotCodexIndex + 1)); } if (normalized === 'resource' || normalized === '/resource' || normalized.startsWith('resource/') || normalized.includes('/resource/')) { diff --git a/src/app/main/mainChatPanel/chatResourceUrl.ts b/src/app/main/mainChatPanel/chatResourceUrl.ts index f3a7a4e..e3e8820 100644 --- a/src/app/main/mainChatPanel/chatResourceUrl.ts +++ b/src/app/main/mainChatPanel/chatResourceUrl.ts @@ -6,6 +6,20 @@ const CHAT_PUBLIC_DOT_CODEX_MARKER = '/public/.codex_chat/'; const RESOURCE_MANAGER_PREVIEW_MARKER = '/api/resource-manager/preview/'; const RESOURCE_MANAGER_ROOT_MARKER = 'resource/'; +function buildCanonicalChatApiResourcePath(relativePath: string) { + const normalizedRelativePath = String(relativePath ?? '').trim().replace(/^\/+/, ''); + + if (!normalizedRelativePath) { + return ''; + } + + if (normalizedRelativePath.startsWith('.codex_chat/')) { + return `${CHAT_API_RESOURCE_MARKER}${normalizedRelativePath}`; + } + + return `${CHAT_API_RESOURCE_MARKER}.codex_chat/${normalizedRelativePath}`; +} + function normalizeUrlFragmentValue(value: string) { const normalized = String(value ?? '').trim().replace(/^#+/, ''); @@ -154,20 +168,20 @@ function extractEmbeddedResourcePath(value: string) { const apiPath = normalized.slice(apiMarkerIndex); const dotCodexIndex = apiPath.indexOf(CHAT_DOT_CODEX_MARKER); return dotCodexIndex >= 0 - ? `${CHAT_API_RESOURCE_MARKER}${apiPath.slice(dotCodexIndex + 1)}` + ? buildCanonicalChatApiResourcePath(apiPath.slice(dotCodexIndex + 1)) : apiPath; } const publicDotCodexIndex = normalized.lastIndexOf(CHAT_PUBLIC_DOT_CODEX_MARKER); if (publicDotCodexIndex >= 0) { - return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(publicDotCodexIndex + 8)}`; + return buildCanonicalChatApiResourcePath(normalized.slice(publicDotCodexIndex + 8)); } const dotCodexIndex = normalized.lastIndexOf(CHAT_DOT_CODEX_MARKER); if (dotCodexIndex >= 0) { - return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(dotCodexIndex + 1)}`; + return buildCanonicalChatApiResourcePath(normalized.slice(dotCodexIndex + 1)); } if (normalized === 'resource' || normalized === '/resource' || normalized.startsWith('resource/') || normalized.includes('/resource/')) { @@ -197,11 +211,11 @@ function extractKnownPreviewPath(value: string) { } if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) { - return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length)}`; + return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length)); } if (pathname.startsWith(CHAT_DOT_CODEX_MARKER)) { - return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_DOT_CODEX_MARKER.length)}`; + return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_DOT_CODEX_MARKER.length)); } return normalized; diff --git a/src/app/main/mainChatPanel/messageParts.ts b/src/app/main/mainChatPanel/messageParts.ts index 7a4d77d..c29cf21 100644 --- a/src/app/main/mainChatPanel/messageParts.ts +++ b/src/app/main/mainChatPanel/messageParts.ts @@ -19,6 +19,20 @@ const CHAT_DOT_CODEX_MARKER = '/.codex_chat/'; const CHAT_PUBLIC_DOT_CODEX_MARKER = '/public/.codex_chat/'; const RESOURCE_MANAGER_PREVIEW_MARKER = '/api/resource-manager/preview/'; const RESOURCE_MANAGER_ROOT_MARKER = 'resource/'; + +function buildCanonicalChatApiResourcePath(relativePath: string) { + const normalizedRelativePath = normalizeText(relativePath).replace(/^\/+/, ''); + + if (!normalizedRelativePath) { + return ''; + } + + if (normalizedRelativePath.startsWith('.codex_chat/')) { + return `${CHAT_API_RESOURCE_MARKER}${normalizedRelativePath}`; + } + + return `${CHAT_API_RESOURCE_MARKER}.codex_chat/${normalizedRelativePath}`; +} type PromptPart = Extract; type PromptOption = PromptPart['options'][number]; type PromptPreview = NonNullable; @@ -174,11 +188,11 @@ function extractKnownPreviewPath(value: string) { } if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) { - return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length)}`; + return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length)); } if (pathname.startsWith(CHAT_DOT_CODEX_MARKER)) { - return `${CHAT_API_RESOURCE_MARKER}${pathname.slice(CHAT_DOT_CODEX_MARKER.length)}`; + return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_DOT_CODEX_MARKER.length)); } return normalized; @@ -210,18 +224,18 @@ function normalizeUrl(value: string) { const apiPath = normalized.slice(apiMarkerIndex); const dotCodexIndex = apiPath.indexOf(CHAT_DOT_CODEX_MARKER); return dotCodexIndex >= 0 - ? `${CHAT_API_RESOURCE_MARKER}${apiPath.slice(dotCodexIndex + 1)}` + ? buildCanonicalChatApiResourcePath(apiPath.slice(dotCodexIndex + 1)) : apiPath; } const publicDotCodexIndex = normalized.lastIndexOf(CHAT_PUBLIC_DOT_CODEX_MARKER); if (publicDotCodexIndex >= 0) { - return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(publicDotCodexIndex + 8)}`; + return buildCanonicalChatApiResourcePath(normalized.slice(publicDotCodexIndex + 8)); } const dotCodexIndex = normalized.lastIndexOf(CHAT_DOT_CODEX_MARKER); if (dotCodexIndex >= 0) { - return `${CHAT_API_RESOURCE_MARKER}${normalized.slice(dotCodexIndex + 1)}`; + return buildCanonicalChatApiResourcePath(normalized.slice(dotCodexIndex + 1)); } if (normalized === 'resource' || normalized === '/resource' || normalized.startsWith('resource/') || normalized.includes('/resource/')) { diff --git a/src/app/main/pages/ChatSharePage.css b/src/app/main/pages/ChatSharePage.css index d3f89ec..26c98ef 100644 --- a/src/app/main/pages/ChatSharePage.css +++ b/src/app/main/pages/ChatSharePage.css @@ -1658,6 +1658,7 @@ border-radius: 999px; color: rgba(30, 41, 59, 0.92); background: rgba(226, 232, 240, 0.72); + flex: 0 0 auto; } .chat-share-page__process-inspector-sections { @@ -1673,8 +1674,9 @@ .chat-share-page__process-inspector-section { display: grid; - gap: 6px; + gap: 8px; min-height: 0; + align-content: start; } .chat-share-page__process-inspector-section--checklist, @@ -1695,6 +1697,14 @@ flex-wrap: wrap; } +.chat-share-page__process-inspector-section-head-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + min-width: 0; +} + .chat-share-page__process-inspector-checklist, .chat-share-page__process-inspector-narratives, .chat-share-page__process-inspector-log { @@ -1797,6 +1807,7 @@ @media (max-width: 960px) { .chat-share-page__process-inspector { width: min(100vw - 16px, 720px); + max-height: min(100dvh - 16px, 760px); } .chat-share-page__process-inspector-sections { @@ -1811,6 +1822,88 @@ } } +@media (max-width: 640px) { + .chat-share-page__process-inspector { + width: calc(100vw - 12px); + max-height: calc(100dvh - 12px); + border-radius: 18px; + } + + .chat-share-page__process-inspector-drag { + align-items: flex-start; + padding: 12px; + } + + .chat-share-page__process-inspector-drag-copy { + width: 100%; + } + + .chat-share-page__process-inspector-window-actions { + flex: 0 0 auto; + } + + .chat-share-page__process-inspector-summary { + gap: 10px; + padding: 12px; + } + + .chat-share-page__process-inspector-sections { + gap: 14px; + padding: 12px; + } + + .chat-share-page__process-inspector-summary-head, + .chat-share-page__process-inspector-section-head { + align-items: flex-start; + } + + .chat-share-page__process-inspector-section-head { + gap: 10px; + } + + .chat-share-page__process-inspector-section-head .ant-typography { + flex: 1 1 auto; + min-width: 0; + } + + .chat-share-page__process-inspector-section-head-actions { + align-self: stretch; + } + + .chat-share-page__process-inspector-table-row, + .chat-share-page__process-inspector-narrative { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + padding: 10px; + } + + .chat-share-page__process-inspector-check-item { + grid-template-columns: minmax(0, 1fr); + gap: 6px; + align-items: start; + padding: 10px; + } + + .chat-share-page__process-inspector-check-item .ant-tag, + .chat-share-page__process-inspector-summary-toggle.ant-btn { + justify-self: flex-start; + } + + .chat-share-page__process-inspector-summary-toggle.ant-btn { + margin-top: 2px; + } + + .chat-share-page__process-inspector-log { + max-height: min(38dvh, 320px); + } + + .chat-share-page__process-inspector-log-line { + grid-template-columns: 28px minmax(0, 1fr); + gap: 8px; + padding: 9px 10px; + } +} + .chat-share-page__process-inspector-minimized { display: flex; align-items: flex-start; diff --git a/src/app/main/pages/ChatSharePage.tsx b/src/app/main/pages/ChatSharePage.tsx index 7a56a52..635c732 100644 --- a/src/app/main/pages/ChatSharePage.tsx +++ b/src/app/main/pages/ChatSharePage.tsx @@ -186,7 +186,7 @@ type ShareNotificationClientStatus = { tone: ShareNotificationStatusTone; }; type ShareProcessInspectorMode = 'default' | 'fullscreen' | 'minimized'; -type ShareProcessInspectorExpandedSection = 'summary' | 'narratives' | null; +type ShareProcessInspectorExpandedSection = 'summary' | 'checklist' | 'narratives' | 'log' | null; type ShareProcessChecklistStep = { key: string; label: string; @@ -5565,7 +5565,7 @@ export function ChatSharePage() { setActiveProcessInspectorRequestId(requestId); setProcessInspectorMode('default'); - setProcessInspectorExpandedSection(null); + setProcessInspectorExpandedSection('checklist'); }, []); const closeProcessInspector = useCallback(() => { @@ -5577,9 +5577,16 @@ export function ChatSharePage() { setProcessInspectorExpandedSection((current) => (current === 'summary' ? null : 'summary')); }, []); + const handleToggleProcessInspectorChecklist = useCallback(() => { + setProcessInspectorExpandedSection((current) => (current === 'checklist' ? null : 'checklist')); + }, []); + const handleToggleProcessInspectorNarratives = useCallback(() => { setProcessInspectorExpandedSection((current) => (current === 'narratives' ? null : 'narratives')); }, []); + const handleToggleProcessInspectorLog = useCallback(() => { + setProcessInspectorExpandedSection((current) => (current === 'log' ? null : 'log')); + }, []); const handleProgramMinimizedPointerDown = useCallback((event: ReactPointerEvent) => { if (event.pointerType === 'mouse' && event.button !== 0) { @@ -7506,7 +7513,9 @@ export function ChatSharePage() { [activeProcessInspectorRequestId, requestById], ); const isProcessInspectorSummaryCollapsed = processInspectorExpandedSection !== 'summary'; + const isProcessInspectorChecklistCollapsed = processInspectorExpandedSection !== 'checklist'; const isProcessInspectorNarrativesCollapsed = processInspectorExpandedSection !== 'narratives'; + const isProcessInspectorLogCollapsed = processInspectorExpandedSection !== 'log'; const activeProcessInspectorPayload = useMemo(() => { if (!activeProcessInspectorRequest) { return null; @@ -7720,26 +7729,37 @@ export function ChatSharePage() {
계획 체크리스트 +
-
- {activeProcessInspectorPayload.checklist.map((step) => ( -
- {step.label} - - {step.status === 'completed' ? '완료' : step.status === 'in_progress' ? '진행중' : '대기'} - - {step.note} -
- ))} -
+ {isProcessInspectorChecklistCollapsed ? null : ( +
+ {activeProcessInspectorPayload.checklist.map((step) => ( +
+ {step.label} + + {step.status === 'completed' ? '완료' : step.status === 'in_progress' ? '진행중' : '대기'} + + {step.note} +
+ ))} +
+ )}
@@ -7768,20 +7788,33 @@ export function ChatSharePage() {
활동 로그 - {activeProcessInspectorPayload.activityLines.length}줄 -
-
- {activeProcessInspectorPayload.activityLines.length > 0 ? ( - activeProcessInspectorPayload.activityLines.map((line, index) => ( -
- {String(index + 1).padStart(2, '0')} - {line} -
- )) - ) : ( - 활동 로그가 아직 기록되지 않았습니다. - )} +
+ {activeProcessInspectorPayload.activityLines.length}줄 + +
+ {isProcessInspectorLogCollapsed ? null : ( +
+ {activeProcessInspectorPayload.activityLines.length > 0 ? ( + activeProcessInspectorPayload.activityLines.map((line, index) => ( +
+ {String(index + 1).padStart(2, '0')} + {line} +
+ )) + ) : ( + 활동 로그가 아직 기록되지 않았습니다. + )} +
+ )}
diff --git a/src/features/serverCommand/ServerCommandPage.tsx b/src/features/serverCommand/ServerCommandPage.tsx index 472b056..e3cc203 100644 --- a/src/features/serverCommand/ServerCommandPage.tsx +++ b/src/features/serverCommand/ServerCommandPage.tsx @@ -8,7 +8,7 @@ import { SyncOutlined, } from '@ant-design/icons'; import { Alert, Button, Empty, Space, Tag, Typography, message } from 'antd'; -import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; +import { useEffect, useEffectEvent, useMemo, useRef, useState, type ReactNode } from 'react'; import { useTokenAccess } from '../../app/main/tokenAccess'; import { DataStatePanel } from '../../components/dataStatePanel'; import { copyText } from '../../app/main/mainChatPanel'; @@ -802,6 +802,13 @@ export function ServerCommandPage({ sharedAccess = null }: ServerCommandPageProp } }; + const refreshServerCommandState = useEffectEvent((options?: { silent?: boolean }) => Promise.all([ + loadItems(options), + loadReservation({ silent: true }), + loadWorkServerDeployment({ silent: true }), + loadTestDeployment({ silent: true }), + ])); + useEffect(() => { if (!hasAccess && !isSharedManageMode) { setItems([]); @@ -813,7 +820,7 @@ export function ServerCommandPage({ sharedAccess = null }: ServerCommandPageProp return; } - void Promise.all([loadItems(), loadReservation({ silent: true }), loadWorkServerDeployment({ silent: true }), loadTestDeployment({ silent: true })]); + void refreshServerCommandState(); }, [allowedKeysKey, hasAccess, isSharedManageMode, sharedAccess?.shareToken]); useEffect(() => { @@ -834,12 +841,7 @@ export function ServerCommandPage({ sharedAccess = null }: ServerCommandPageProp const refresh = async () => { try { - await Promise.all([ - loadItems({ silent: true }), - loadReservation({ silent: true }), - loadWorkServerDeployment({ silent: true }), - loadTestDeployment({ silent: true }), - ]); + await refreshServerCommandState({ silent: true }); } catch { if (!cancelled) { // ignore polling errors and keep the latest visible state @@ -858,6 +860,28 @@ export function ServerCommandPage({ sharedAccess = null }: ServerCommandPageProp }; }, [hasAccess, isSharedManageMode, runningActionKey, sharedAccess?.shareToken, testDeployment?.status, workServerDeployment?.status]); + useEffect(() => { + if (!hasAccess && !isSharedManageMode) { + return undefined; + } + + const handleWindowAttention = () => { + if (document.visibilityState !== 'visible') { + return; + } + + void refreshServerCommandState({ silent: true }); + }; + + window.addEventListener('focus', handleWindowAttention); + document.addEventListener('visibilitychange', handleWindowAttention); + + return () => { + window.removeEventListener('focus', handleWindowAttention); + document.removeEventListener('visibilitychange', handleWindowAttention); + }; + }, [hasAccess, isSharedManageMode, refreshServerCommandState]); + useEffect(() => { if (!workServerDeployment || workServerDeployment.status === 'idle' || workServerDeployment.status === 'running') { return; @@ -874,6 +898,7 @@ export function ServerCommandPage({ sharedAccess = null }: ServerCommandPageProp } if (workServerDeployment.status === 'completed') { + void refreshServerCommandState({ silent: true }); void messageApi.success('WORK 서버 무중단 배포가 완료되었습니다.'); return; }