chore: test deploy snapshot

This commit is contained in:
2026-05-28 14:34:49 +09:00
parent 82c46f4be4
commit bb275c0534
14 changed files with 531 additions and 193 deletions

1
.tmp-chatshare-full.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -66,6 +66,7 @@ import { getTokenSettingById, type TokenSettingRecord } from '../services/token-
const CHAT_ATTACHMENT_FILE_SIZE_LIMIT = 300 * 1024 * 1024;
const CHAT_ATTACHMENT_ROUTE_BODY_LIMIT = 450 * 1024 * 1024;
const CHAT_PUBLIC_ROUTE_PREFIX = '/.codex_chat/';
const CHAT_LEGACY_PUBLIC_ROUTE_PREFIX = '/public/.codex_chat/';
const CHAT_API_RESOURCE_ROUTE_PREFIX = '/api/chat/resources';
const CHAT_SHARE_ROUTE_PREFIX = '/api/chat/shares';
const CHAT_SHARE_TOKEN_VERSION = 1;
@@ -1235,7 +1236,11 @@ async function buildChatShareSnapshot(
tokenPayload.kind === 'request-bundle' && normalizedSessionId.startsWith(MANAGED_CHAT_SHARE_SESSION_PREFIX);
const useInitialManagedShareRoomView = isManagedShareRoomSession && detailLevel === 'initial';
const detailPage = useInitialManagedShareRoomView
? await listChatConversationDetailPage(normalizedSessionId, { limit: 12 })
? await listChatConversationDetailPage(normalizedSessionId, {
limit: 12,
includeActivityLogs: false,
includePagination: false,
})
: null;
const [requests, messages] = detailPage
? [detailPage.requests, detailPage.messages]
@@ -1571,7 +1576,7 @@ async function ensureManagedShareAccessPin(
});
if (pinStatus.status === 'ok' || pinStatus.status === 'not-configured') {
return true;
return pinStatus;
}
if (pinStatus.status === 'required') {
@@ -1579,7 +1584,7 @@ async function ensureManagedShareAccessPin(
code: 'share_pin_required',
message: '이 공유 채팅방은 4자리 비밀번호 입력이 필요합니다.',
});
return false;
return null;
}
if (pinStatus.status === 'invalid') {
@@ -1587,17 +1592,17 @@ async function ensureManagedShareAccessPin(
code: 'share_pin_invalid',
message: '공유 채팅방 비밀번호가 올바르지 않습니다.',
});
return false;
return null;
}
if (pinStatus.status === 'missing') {
reply.code(404).send({
message: '공유 링크를 찾을 수 없습니다.',
});
return false;
return null;
}
return true;
return null;
}
function hasManagedSharePermission(
@@ -1641,6 +1646,11 @@ export async function registerChatRoutes(app: FastifyInstance) {
return serveChatPublicResource(resolveChatAttachmentRepoPath(), wildcard, reply);
});
app.get(`${CHAT_LEGACY_PUBLIC_ROUTE_PREFIX}*`, async (request, reply) => {
const wildcard = String((request.params as { '*': string | undefined })['*'] ?? '').trim();
return serveChatPublicResource(resolveChatAttachmentRepoPath(), wildcard, reply);
});
app.get(`${CHAT_API_RESOURCE_ROUTE_PREFIX}/*`, async (request, reply) => {
const wildcard = String((request.params as { '*': string | undefined })['*'] ?? '').trim();
return serveChatPublicResource(resolveChatAttachmentRepoPath(), wildcard, reply);
@@ -1668,7 +1678,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -1898,7 +1910,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -2033,7 +2047,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -2467,7 +2483,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
const accessPinStatus = await ensureManagedShareAccessPin(request, reply, managedContext.sharePath);
if (!accessPinStatus) {
return;
}
@@ -2497,15 +2515,19 @@ export async function registerChatRoutes(app: FastifyInstance) {
});
}
const accessPinStatus = await validateSharedResourceAccessPinBySharePath(managedContext.sharePath, getRequestChatSharePin(request), {
clientId: getRequestClientId(request),
});
if (managedContext.managedResource) {
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
void recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
actorLabel: 'share-viewer',
summary: '공유 채팅 링크를 열었습니다.',
detail: managedContext.managedResource.token.resourceLabel,
}).catch((error) => {
request.log.warn(
{
err: error,
managedResourceTokenId: managedContext.managedResource?.token.id,
},
'Failed to record shared chat view usage',
);
});
}

View File

@@ -0,0 +1,39 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { extractChatMessageParts, parseChatMessageParts } from './chat-message-parts.js';
test('extractChatMessageParts normalizes absolute legacy dot-codex prompt preview urls to api chat resource urls', () => {
const input = [
'문서 미리보기',
'[[prompt:{"title":"확인","options":[{"label":"legacy","value":"legacy","preview":{"type":"resource","url":"https://preview.sm-home.cloud/public/.codex_chat/chat-room/resource/source/chat-room-reference.md"}}]}]]',
].join('\n');
const parsed = extractChatMessageParts(input);
const prompt = parsed.parts.find((part): part is Extract<(typeof parsed.parts)[number], { type: 'prompt' }> => part.type === 'prompt');
assert.ok(prompt);
assert.equal(
prompt.options[0]?.preview?.url,
'/api/chat/resources/chat-room/resource/source/chat-room-reference.md',
);
});
test('parseChatMessageParts normalizes absolute legacy link card urls to api chat resource urls', () => {
const parsed = parseChatMessageParts([
{
type: 'link_card',
title: 'legacy resource',
url: 'https://preview.sm-home.cloud/.codex_chat/chat-room/resource/uploads/spec.png',
actionLabel: '열기',
},
]);
assert.deepEqual(parsed, [
{
type: 'link_card',
title: 'legacy resource',
url: '/api/chat/resources/chat-room/resource/uploads/spec.png',
actionLabel: '열기',
},
]);
});

View File

@@ -237,6 +237,14 @@ function normalizeUrl(value: string) {
if (pathname.startsWith(CHAT_API_RESOURCE_MARKER) || pathname.startsWith(RESOURCE_MANAGER_PREVIEW_MARKER)) {
return normalizePreviewPathHash(pathname);
}
if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) {
return `${CHAT_API_RESOURCE_MARKER}${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)}`;
}
} catch {
// Fall through to handle relative and embedded resource paths below.
}

View File

@@ -3570,12 +3570,16 @@ export async function listChatConversationDetailPage(
options: {
limit?: number;
beforeMessageId?: number | null;
includeActivityLogs?: boolean;
includePagination?: boolean;
} = {},
): Promise<ChatConversationDetailPage> {
const normalizedSessionId = sessionId.trim();
await getChatConversation(normalizedSessionId, null);
const conversation = await db(CHAT_CONVERSATION_TABLE).where({ session_id: normalizedSessionId }).first();
const normalizedLimit = Math.max(1, Math.min(100, Math.round(options.limit ?? 10)));
const includeActivityLogs = options.includeActivityLogs !== false;
const includePagination = options.includePagination !== false;
const normalizedBeforeMessageId =
Number.isFinite(options.beforeMessageId) && (options.beforeMessageId ?? 0) > 0
? Math.trunc(options.beforeMessageId as number)
@@ -3629,21 +3633,26 @@ export async function listChatConversationDetailPage(
.orderBy('message_id', 'asc')
.orderBy('id', 'asc');
const messages = messageRows.map((row: Parameters<typeof mapMessageRow>[0]) => mapMessageRow(row));
const activityLogs = await listChatConversationActivityLogsByRequestIds(normalizedSessionId, requestIds);
const oldestLoadedMessageId =
requests.reduce<number | null>((oldestId, request) => {
const candidateIds = [request.userMessageId, request.responseMessageId].filter(
(value): value is number => typeof value === 'number' && Number.isInteger(value) && value > 0,
);
const activityLogs = includeActivityLogs
? await listChatConversationActivityLogsByRequestIds(normalizedSessionId, requestIds)
: [];
const oldestLoadedMessageId = includePagination
? (
requests.reduce<number | null>((oldestId, request) => {
const candidateIds = [request.userMessageId, request.responseMessageId].filter(
(value): value is number => typeof value === 'number' && Number.isInteger(value) && value > 0,
);
if (candidateIds.length === 0) {
return oldestId;
}
if (candidateIds.length === 0) {
return oldestId;
}
const nextCandidateId = Math.min(...candidateIds);
return oldestId == null ? nextCandidateId : Math.min(oldestId, nextCandidateId);
}, null) ?? messages[0]?.id ?? null;
const oldestRequest = requests[0] ?? null;
const nextCandidateId = Math.min(...candidateIds);
return oldestId == null ? nextCandidateId : Math.min(oldestId, nextCandidateId);
}, null) ?? messages[0]?.id ?? null
)
: null;
const oldestRequest = includePagination ? requests[0] ?? null : null;
const hasOlderMessages = oldestRequest
? Boolean(
await db(CHAT_CONVERSATION_REQUEST_TABLE)

View File

@@ -9,7 +9,7 @@ const port = Number(process.env.PORT ?? 5173);
const distDirName = process.env.APP_DIST_DIR ?? 'app-dist';
const rootDir = normalize(isAbsolute(distDirName) ? distDirName : join(process.cwd(), distDirName));
const workServerUrl = new URL(process.env.WORK_SERVER_URL ?? 'http://127.0.0.1:3100');
const proxyPrefixes = ['/api', '/.codex_chat', '/ws/chat'];
const proxyPrefixes = ['/api', '/.codex_chat', '/public/.codex_chat', '/ws/chat'];
const mimeTypes = {
'.css': 'text/css; charset=utf-8',

View File

@@ -86,6 +86,10 @@ async function resetServiceWorkersAndReload() {
reloadAppWithCacheBuster();
}
export async function forceReloadApp() {
await resetServiceWorkersAndReload();
}
async function unregisterServiceWorkers() {
if (!isAppUpdateSupported()) {
return;

View File

@@ -196,6 +196,14 @@ function extractKnownPreviewPath(value: string) {
return normalizePreviewPathHash(pathname);
}
if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) {
return `${CHAT_API_RESOURCE_MARKER}${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 normalized;
} catch {
return normalizePreviewPathHash(extractEmbeddedResourcePath(normalized));

View File

@@ -173,6 +173,14 @@ function extractKnownPreviewPath(value: string) {
return normalizePreviewPathHash(pathname);
}
if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) {
return `${CHAT_API_RESOURCE_MARKER}${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 normalized;
} catch {
return '';

View File

@@ -224,7 +224,7 @@
.chat-share-page__room-list-panel--floating {
position: fixed;
z-index: 1300;
z-index: 1700;
overflow: hidden;
box-shadow:
0 18px 42px rgba(15, 23, 42, 0.18),
@@ -886,15 +886,29 @@
display: inline-flex;
align-items: center;
gap: 8px;
width: auto;
max-width: 100%;
align-self: flex-start;
min-width: 0;
flex-wrap: wrap;
flex-wrap: nowrap;
}
.chat-share-page__conversation-title.ant-typography {
margin: 0;
flex: 0 1 auto;
max-width: 100%;
min-width: 0;
font-size: 18px;
line-height: 1.35;
font-weight: 700;
color: #0f172a;
}
.chat-share-page__header-summary.ant-typography {
margin: 0;
font-size: 11px;
line-height: 1.35;
white-space: nowrap;
white-space: normal;
flex: 0 0 auto;
}
@@ -914,6 +928,55 @@
background: #ef4444;
}
.chat-share-page__status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0;
max-width: min(100%, 96px);
min-height: 18px;
padding: 1px 6px;
border-radius: 999px;
flex: 0 0 auto;
font-size: 10px;
font-weight: 600;
line-height: 1.2;
color: #334155;
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.28);
}
.chat-share-page__status-badge--processing {
background: rgba(219, 234, 254, 0.92);
}
.chat-share-page__status-badge--warning {
background: rgba(254, 243, 199, 0.9);
}
.chat-share-page__status-badge--success {
background: rgba(220, 252, 231, 0.92);
}
.chat-share-page__status-badge--error {
background: rgba(254, 226, 226, 0.92);
}
.chat-share-page__status-badge--default {
background: rgba(241, 245, 249, 0.96);
}
.chat-share-page__status-badge-label {
display: block;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.chat-share-page__section-action--title-edit.ant-btn {
flex: 0 0 auto;
}
.chat-share-page__section-actions {
display: inline-flex;
align-items: center;
@@ -929,7 +992,7 @@
.chat-share-page__request-nav {
display: inline-flex;
align-items: center;
gap: 2px;
gap: 4px;
}
.chat-share-page__section-action--icon.ant-btn {
@@ -999,6 +1062,34 @@
transform: translateY(0);
}
.chat-share-page__section-action--inline.ant-btn {
height: 28px;
padding-inline: 8px;
box-shadow: none;
background: transparent;
}
.chat-share-page__section-action--inline.ant-btn .ant-btn-icon {
width: 20px;
height: 20px;
background: rgba(226, 232, 240, 0.72);
color: #475569;
box-shadow: none;
}
.chat-share-page__section-action--inline.ant-btn:hover,
.chat-share-page__section-action--inline.ant-btn:focus-visible {
background: transparent;
box-shadow: none;
transform: none;
}
.chat-share-page__section-action--inline.ant-btn:hover .ant-btn-icon,
.chat-share-page__section-action--inline.ant-btn:focus-visible .ant-btn-icon {
background: rgba(219, 234, 254, 0.92);
color: #1d4ed8;
}
.chat-share-page__tool-button-label {
font-size: 12px;
font-weight: 700;
@@ -1211,20 +1302,33 @@
}
.chat-share-page__expand-mode-filter.ant-btn {
min-width: 34px;
color: #475569;
background: transparent;
box-shadow: none;
padding-inline: 8px;
}
.chat-share-page__expand-mode-filter--active.ant-btn {
background: linear-gradient(180deg, rgba(219, 234, 254, 0.98) 0%, rgba(191, 219, 254, 0.94) 100%);
color: #1d4ed8;
box-shadow:
inset 0 0 0 1px rgba(96, 165, 250, 0.28),
0 8px 18px rgba(96, 165, 250, 0.14);
background: rgba(219, 234, 254, 0.38);
box-shadow: inset 0 0 0 1px rgba(96, 165, 250, 0.18);
}
.chat-share-page__expand-mode-filter.ant-btn:hover {
.chat-share-page__expand-mode-filter.ant-btn:hover,
.chat-share-page__expand-mode-filter.ant-btn:focus-visible {
color: #1d4ed8;
background: rgba(226, 232, 240, 0.72);
background: rgba(226, 232, 240, 0.5);
box-shadow: none;
transform: none;
}
.chat-share-page__expand-mode-filter.ant-btn:active {
transform: none;
}
.chat-share-page__expand-mode-filter.ant-btn .chat-share-page__tool-button-label {
color: inherit;
}
.chat-share-page__expand-mode-filter.ant-btn {
@@ -1232,8 +1336,9 @@
}
.chat-share-page__expand-mode-filter.ant-btn .ant-btn-icon {
color: #1d4ed8;
background: rgba(219, 234, 254, 0.96);
color: currentColor;
background: transparent;
box-shadow: none;
}
.chat-share-page__expand-mode-filter.ant-btn .anticon {
@@ -2656,67 +2761,72 @@
}
.chat-share-page__first-inquiry {
display: grid;
gap: 8px;
padding: 2px 0 12px;
border-bottom: 1px solid rgba(148, 163, 184, 0.28);
}
.chat-share-page__first-inquiry-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
}
.chat-share-page__first-inquiry-copy {
display: grid;
gap: 8px;
min-width: 0;
}
.chat-share-page__first-inquiry-title-row {
.chat-share-page__conversation-toolbar {
display: flex;
align-items: flex-start;
gap: 6px;
min-width: 0;
justify-content: flex-end;
margin: 0 -10px 8px;
padding: 2px 10px 0;
}
.chat-share-page__first-inquiry-title-row .chat-share-page__section-action.ant-btn {
flex: 0 0 auto;
margin-top: -2px;
.chat-share-page__conversation-toolbar {
position: sticky;
top: var(--chat-share-page-conversation-toolbar-top, 52px);
z-index: 18;
background: transparent;
backdrop-filter: none;
}
.chat-share-page__first-inquiry-menu-badge {
.chat-share-page__conversation-toolbar-group {
display: inline-flex;
align-items: center;
width: fit-content;
justify-content: flex-end;
gap: 4px;
max-width: 100%;
min-height: 28px;
padding: 0 12px;
border-radius: 999px;
background: rgba(219, 234, 254, 0.94);
color: #1d4ed8;
box-shadow: inset 0 0 0 1px rgba(96, 165, 250, 0.22);
font-size: 12px;
font-weight: 700;
line-height: 1.2;
}
.chat-share-page__first-inquiry-title.ant-typography {
margin: 0;
min-width: 0;
font-size: 18px;
line-height: 1.45;
font-weight: 700;
.chat-share-page__conversation-toolbar-button.ant-btn {
min-width: 34px;
height: 34px;
padding-inline: 8px;
color: #475569;
background: transparent;
border-color: rgba(148, 163, 184, 0.22);
box-shadow: none;
backdrop-filter: none;
}
.chat-share-page__conversation-toolbar-button.ant-btn .ant-btn-icon {
color: currentColor;
background: transparent;
box-shadow: none;
}
.chat-share-page__conversation-toolbar-button.ant-btn:hover,
.chat-share-page__conversation-toolbar-button.ant-btn:focus-visible {
color: #0f172a;
overflow: hidden;
background: transparent;
border-color: rgba(100, 116, 139, 0.34);
box-shadow: none;
}
.chat-share-page__first-inquiry-head .chat-share-page__expand-mode-filter.ant-btn {
justify-self: end;
margin-top: 1px;
.chat-share-page__conversation-toolbar-button.ant-btn:active {
background: transparent;
border-color: rgba(71, 85, 105, 0.42);
box-shadow: none;
transform: none;
}
.chat-share-page__conversation-toolbar-button.ant-btn:disabled,
.chat-share-page__conversation-toolbar-button.ant-btn:disabled:hover,
.chat-share-page__conversation-toolbar-button.ant-btn:disabled:focus-visible {
color: rgba(100, 116, 139, 0.68);
background: transparent;
border-color: rgba(148, 163, 184, 0.16);
box-shadow: none;
}
.chat-share-page__conversation-toolbar .chat-share-page__conversation-toolbar-button.ant-btn .chat-share-page__tool-button-label {
color: inherit;
}
.chat-share-page__bundle-list {

View File

@@ -65,6 +65,7 @@ import { extractPreviewItems, type PreviewItem } from '../mainChatPanel/previewI
import { buildChatPath, buildPlayAppPath } from '../routes';
import type { PreviewKind } from '../mainChatPanel/previewKind';
import { normalizeChatResourceUrl } from '../mainChatPanel/chatResourceUrl';
import { forceReloadApp } from '../appUpdate';
import type {
ChatComposerAttachment,
ChatConversationSummary,
@@ -185,6 +186,7 @@ type ShareNotificationClientStatus = {
tone: ShareNotificationStatusTone;
};
type ShareProcessInspectorMode = 'default' | 'fullscreen' | 'minimized';
type ShareProcessInspectorExpandedSection = 'summary' | 'narratives' | null;
type ShareProcessChecklistStep = {
key: string;
label: string;
@@ -4030,6 +4032,7 @@ export function ChatSharePage() {
const [pendingRequestRetryIds, setPendingRequestRetryIds] = useState<string[]>([]);
const [isShareRoomListVisible, setIsShareRoomListVisible] = useState(false);
const [shareRoomListLayerStyle, setShareRoomListLayerStyle] = useState<CSSProperties | null>(null);
const [conversationToolbarStickyTop, setConversationToolbarStickyTop] = useState(52);
const [isRoomSwitching, setIsRoomSwitching] = useState(false);
const [replyReferenceRequestId, setReplyReferenceRequestId] = useState('');
const [previousQuestionModalRequestId, setPreviousQuestionModalRequestId] = useState('');
@@ -4080,8 +4083,7 @@ export function ChatSharePage() {
const [pendingShareRuntimeRequestIds, setPendingShareRuntimeRequestIds] = useState<string[]>([]);
const [activeProcessInspectorRequestId, setActiveProcessInspectorRequestId] = useState('');
const [processInspectorMode, setProcessInspectorMode] = useState<ShareProcessInspectorMode>('default');
const [isProcessInspectorSummaryCollapsed, setIsProcessInspectorSummaryCollapsed] = useState(true);
const [isProcessInspectorNarrativesCollapsed, setIsProcessInspectorNarrativesCollapsed] = useState(true);
const [processInspectorExpandedSection, setProcessInspectorExpandedSection] = useState<ShareProcessInspectorExpandedSection>(null);
const [optimisticShareRooms, setOptimisticShareRooms] = useState<ChatShareRoomSummary[]>([]);
const [shareRoomPendingCountsBySessionId, setShareRoomPendingCountsBySessionId] = useState<Record<string, ShareRoomPendingCounts>>({});
const [isLoadingShareRoomPendingCounts, setIsLoadingShareRoomPendingCounts] = useState(false);
@@ -4102,6 +4104,7 @@ export function ChatSharePage() {
const shareRoomPendingCountRefreshPromiseBySessionIdRef = useRef<Record<string, Promise<void> | null>>({});
const shareRoomPendingCountRefreshQueuedBySessionIdRef = useRef<Record<string, boolean>>({});
const conversationHeaderRef = useRef<HTMLDivElement | null>(null);
const conversationToolbarRef = useRef<HTMLDivElement | null>(null);
const roomListTriggerButtonRef = useRef<HTMLButtonElement | null>(null);
const roomListPanelRef = useRef<HTMLElement | null>(null);
const processInspectorCardRef = useRef<HTMLDivElement | null>(null);
@@ -5428,6 +5431,37 @@ export function ChatSharePage() {
};
}, [minimizedPrograms.length]);
useLayoutEffect(() => {
const headerElement = conversationHeaderRef.current;
if (!headerElement) {
return undefined;
}
const updateStickyOffset = () => {
const headerHeight = headerElement.getBoundingClientRect().height;
const nextStickyTop = Math.max(48, Math.ceil(headerHeight) + 8);
setConversationToolbarStickyTop((current) => (current === nextStickyTop ? current : nextStickyTop));
};
const resizeObserver =
typeof ResizeObserver === 'undefined'
? null
: new ResizeObserver(() => {
updateStickyOffset();
});
updateStickyOffset();
window.addEventListener('resize', updateStickyOffset);
resizeObserver?.observe(headerElement);
return () => {
window.removeEventListener('resize', updateStickyOffset);
resizeObserver?.disconnect();
};
}, []);
useLayoutEffect(() => {
if (!isShareRoomListVisible) {
setShareRoomListLayerStyle(null);
@@ -5437,7 +5471,8 @@ export function ChatSharePage() {
const updateLayerPosition = () => {
const triggerRect = roomListTriggerButtonRef.current?.getBoundingClientRect() ?? null;
const headerRect = conversationHeaderRef.current?.getBoundingClientRect() ?? null;
const anchorRect = headerRect ?? triggerRect;
const toolbarRect = conversationToolbarRef.current?.getBoundingClientRect() ?? null;
const anchorRect = triggerRect ?? headerRect ?? toolbarRect;
if (!anchorRect) {
return;
@@ -5445,14 +5480,17 @@ export function ChatSharePage() {
const viewportPadding = 8;
const availableWidth = Math.max(280, window.innerWidth - (viewportPadding * 2));
const preferredWidth = headerRect
? Math.min(Math.max(headerRect.width, 280), 420)
const preferredWidth = toolbarRect
? Math.min(Math.max(toolbarRect.width, 280), 420)
: headerRect
? Math.min(Math.max(headerRect.width, 280), 420)
: Math.min(360, availableWidth);
const width = Math.min(preferredWidth, availableWidth);
const preferredLeft = headerRect?.left ?? triggerRect?.left ?? viewportPadding;
const preferredRight = triggerRect?.right ?? toolbarRect?.right ?? headerRect?.right ?? (viewportPadding + width);
const minLeft = viewportPadding;
const maxLeft = Math.max(viewportPadding, window.innerWidth - viewportPadding - width);
const left = Math.min(Math.max(preferredLeft, viewportPadding), maxLeft);
const top = Math.max(anchorRect.bottom, triggerRect?.bottom ?? 0) + 8;
const left = Math.min(Math.max(preferredRight - width, minLeft), maxLeft);
const top = anchorRect.bottom + 8;
const maxHeight = Math.max(220, window.innerHeight - top - viewportPadding);
setShareRoomListLayerStyle({
@@ -5527,14 +5565,20 @@ export function ChatSharePage() {
setActiveProcessInspectorRequestId(requestId);
setProcessInspectorMode('default');
setIsProcessInspectorSummaryCollapsed(true);
setIsProcessInspectorNarrativesCollapsed(true);
setProcessInspectorExpandedSection(null);
}, []);
const closeProcessInspector = useCallback(() => {
setActiveProcessInspectorRequestId('');
setIsProcessInspectorSummaryCollapsed(true);
setIsProcessInspectorNarrativesCollapsed(true);
setProcessInspectorExpandedSection(null);
}, []);
const handleToggleProcessInspectorSummary = useCallback(() => {
setProcessInspectorExpandedSection((current) => (current === 'summary' ? null : 'summary'));
}, []);
const handleToggleProcessInspectorNarratives = useCallback(() => {
setProcessInspectorExpandedSection((current) => (current === 'narratives' ? null : 'narratives'));
}, []);
const handleProgramMinimizedPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
@@ -5583,7 +5627,7 @@ export function ChatSharePage() {
return;
}
window.location.reload();
void forceReloadApp();
}, []);
const handleReloadProgram = useCallback(() => {
@@ -7461,6 +7505,8 @@ export function ChatSharePage() {
() => (activeProcessInspectorRequestId.trim() ? requestById.get(activeProcessInspectorRequestId.trim()) ?? null : null),
[activeProcessInspectorRequestId, requestById],
);
const isProcessInspectorSummaryCollapsed = processInspectorExpandedSection !== 'summary';
const isProcessInspectorNarrativesCollapsed = processInspectorExpandedSection !== 'narratives';
const activeProcessInspectorPayload = useMemo(() => {
if (!activeProcessInspectorRequest) {
return null;
@@ -7638,9 +7684,7 @@ export function ChatSharePage() {
size="small"
className="chat-share-page__process-inspector-summary-toggle"
icon={isProcessInspectorSummaryCollapsed ? <DownOutlined /> : <UpOutlined />}
onClick={() => {
setIsProcessInspectorSummaryCollapsed((current) => !current);
}}
onClick={handleToggleProcessInspectorSummary}
>
{isProcessInspectorSummaryCollapsed ? '요청 정보 보기' : '요청 정보 접기'}
</Button>
@@ -7705,9 +7749,7 @@ export function ChatSharePage() {
size="small"
className="chat-share-page__process-inspector-summary-toggle"
icon={isProcessInspectorNarrativesCollapsed ? <DownOutlined /> : <UpOutlined />}
onClick={() => {
setIsProcessInspectorNarrativesCollapsed((current) => !current);
}}
onClick={handleToggleProcessInspectorNarratives}
>
{isProcessInspectorNarrativesCollapsed ? '보기' : '접기'}
</Button>
@@ -8686,8 +8728,8 @@ export function ChatSharePage() {
key: 'conversation-refresh',
label: (
<span className="chat-share-page__settings-item">
<span className="chat-share-page__settings-item-title"> </span>
<span className="chat-share-page__settings-item-description">PWA .</span>
<span className="chat-share-page__settings-item-title"> </span>
<span className="chat-share-page__settings-item-description"> .</span>
</span>
),
icon: <ReloadOutlined />,
@@ -9249,19 +9291,46 @@ export function ChatSharePage() {
<div className={contentLayoutClassName}>
<section className="chat-share-page__panel chat-share-page__conversation-panel">
<div ref={conversationHeaderRef} className="chat-share-page__section-head">
<div className="chat-share-page__section-copy">
<div className="chat-share-page__section-copy">
<div className="chat-share-page__section-title-row">
<Title level={5}></Title>
<Title
level={5}
className="chat-share-page__conversation-title"
ellipsis={{ rows: 1, tooltip: headerTitleText.trim() || '채팅' }}
>
{headerTitleText.trim() || '채팅'}
</Title>
{canOpenSharedRoomSettings ? (
<Button
type="text"
size="small"
className="chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__section-action--inline chat-share-page__section-action--title-edit"
aria-label="채팅방 이름 및 설정 편집"
title="채팅방 이름 및 설정 편집"
icon={<EditOutlined />}
onClick={() => {
openSharedRoomSettings();
}}
/>
) : null}
<span
className={`chat-share-page__live-dot ${isLiveConnected ? 'chat-share-page__live-dot--connected' : 'chat-share-page__live-dot--disconnected'}`}
aria-label={isLiveConnected ? '웹소켓 연결됨' : '웹소켓 연결 끊김'}
title={isLiveConnected ? '웹소켓 연결됨' : '웹소켓 연결 끊김'}
/>
{aggregateStatusTag ? <Tag color={aggregateStatusTag.color}>{aggregateStatusTag.label}</Tag> : null}
<Text type="secondary" className="chat-share-page__header-summary">
{headerSummaryLabel}
</Text>
{aggregateStatusTag ? (
<span
className={`chat-share-page__status-badge chat-share-page__status-badge--${aggregateStatusTag.color}`}
aria-label={aggregateStatusTag.label}
title={aggregateStatusTag.label}
>
<span className="chat-share-page__status-badge-label">{aggregateStatusTag.label}</span>
</span>
) : null}
</div>
<Text type="secondary" className="chat-share-page__header-summary">
{headerSummaryLabel}
</Text>
</div>
<div className="chat-share-page__section-actions">
{canToggleShareRoomList ? (
@@ -9278,29 +9347,6 @@ export function ChatSharePage() {
}}
/>
) : null}
<div className="chat-share-page__request-nav" aria-label="요청 이동">
<Button
type="text"
size="small"
className="chat-share-page__section-action"
icon={<LeftOutlined />}
disabled={!canMoveToPreviousRequest}
onClick={handleMoveToPreviousRequest}
>
</Button>
<Button
type="text"
size="small"
className="chat-share-page__section-action"
icon={<RightOutlined />}
iconPosition="end"
disabled={!canMoveToNextRequest}
onClick={handleMoveToNextRequest}
>
</Button>
</div>
<Dropdown
trigger={['click']}
menu={{
@@ -9323,6 +9369,64 @@ export function ChatSharePage() {
</Dropdown>
</div>
</div>
<div
ref={conversationToolbarRef}
className="chat-share-page__conversation-toolbar"
style={
{
'--chat-share-page-conversation-toolbar-top': `${conversationToolbarStickyTop}px`,
} as CSSProperties
}
>
<div className="chat-share-page__conversation-toolbar-group" aria-label="요청 이동 및 필터">
<div className="chat-share-page__request-nav" aria-label="요청 이동">
<Button
type="text"
size="small"
className="chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__conversation-toolbar-button"
icon={<LeftOutlined />}
disabled={!canMoveToPreviousRequest}
onClick={handleMoveToPreviousRequest}
>
<span className="chat-share-page__tool-button-label"></span>
</Button>
<Button
type="text"
size="small"
className="chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__conversation-toolbar-button"
icon={<RightOutlined />}
iconPosition="end"
disabled={!canMoveToNextRequest}
onClick={handleMoveToNextRequest}
>
<span className="chat-share-page__tool-button-label"></span>
</Button>
</div>
<Dropdown
trigger={['click']}
placement="bottomRight"
menu={{
items: shareExpandModeMenuItems,
selectable: true,
selectedKeys: [expandMode],
onClick: handleSelectExpandMode,
}}
>
<Button
type="text"
size="small"
className={`chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__conversation-toolbar-button chat-share-page__expand-mode-filter${expandMode !== 'latest' ? ' chat-share-page__expand-mode-filter--active' : ''}`}
aria-label={`공유 채팅 펼치기 필터: ${getShareExpandModeLabel(expandMode)} ${requestProgressLabel}`.trim()}
title={`공유 채팅 펼치기 필터: ${getShareExpandModeLabel(expandMode)} ${requestProgressLabel}`.trim()}
icon={<FilterOutlined />}
>
<span className="chat-share-page__tool-button-label">
{expandMode === 'latest' ? '필터' : getShareExpandModeLabel(expandMode)}
</span>
</Button>
</Dropdown>
</div>
</div>
{showRoomSwitchingSkeleton ? (
<div className="chat-share-page__conversation-loading-block" role="status" aria-live="polite">
<Spin size="large" />
@@ -9339,59 +9443,6 @@ export function ChatSharePage() {
<Text type="secondary">{`${roomSwitchingStatusLabel} 불러오는 중`}</Text>
</div>
) : null}
{headerInquiryRequest ? (
<section className="chat-share-page__first-inquiry">
<div className="chat-share-page__first-inquiry-head">
<div className="chat-share-page__first-inquiry-copy">
<div className="chat-share-page__first-inquiry-title-row">
<Title
level={5}
className="chat-share-page__first-inquiry-title"
ellipsis={{ rows: 1, tooltip: headerTitleText }}
>
{headerTitleText}
</Title>
{canOpenSharedRoomSettings ? (
<Button
type="text"
size="small"
className="chat-share-page__section-action chat-share-page__section-action--tool"
aria-label="채팅방 이름 및 설정 편집"
title="채팅방 이름 및 설정 편집"
icon={<EditOutlined />}
onClick={() => {
openSharedRoomSettings();
}}
/>
) : null}
</div>
</div>
<Dropdown
trigger={['click']}
placement="bottomRight"
menu={{
items: shareExpandModeMenuItems,
selectable: true,
selectedKeys: [expandMode],
onClick: handleSelectExpandMode,
}}
>
<Button
type="text"
size="small"
className={`chat-share-page__section-action chat-share-page__section-action--tool chat-share-page__expand-mode-filter${expandMode !== 'latest' ? ' chat-share-page__expand-mode-filter--active' : ''}`}
aria-label={`공유 채팅 펼치기 필터: ${getShareExpandModeLabel(expandMode)} ${requestProgressLabel}`.trim()}
title={`공유 채팅 펼치기 필터: ${getShareExpandModeLabel(expandMode)} ${requestProgressLabel}`.trim()}
icon={<FilterOutlined />}
>
<span className="chat-share-page__tool-button-label">
{expandMode === 'latest' ? '필터' : getShareExpandModeLabel(expandMode)}
</span>
</Button>
</Dropdown>
</div>
</section>
) : null}
{expandMode === 'latest' && hiddenBeforeCount > 0 ? (
<div className="chat-share-page__omission chat-share-page__omission--collapsed chat-share-page__omission--before" aria-label={`위쪽 채팅 ${hiddenBeforeCount}건 숨김`}>
<span className="chat-share-page__omission-line" aria-hidden="true" />

View File

@@ -217,6 +217,10 @@ export default defineConfig({
target: WORK_SERVER_HTTP_TARGET,
changeOrigin: true,
},
'/public/.codex_chat': {
target: WORK_SERVER_HTTP_TARGET,
changeOrigin: true,
},
},
},
preview: {