feat(my-deepagent): v0.3 PR #8 — conversation-centric Web GUI (/conversation.html)
Workflow run 페이지를 archive 로 격하시키고, 사용자가 처음 보는 화면을
chat-style 대화 thread 로 전환. Claude Code 의 Web GUI 와 동일한 UX.
핵심 동작:
- 새 페이지 `/conversation.html` 에서 세션을 picker 로 고르거나 "새 대화"
버튼으로 만들고 메시지 입력. Cmd/Ctrl+Enter 로 전송.
- POST /api/sessions/{id}/messages 가 user MessageRow 를 영속한 즉시 200 응답
후 `asyncio.create_task(invoke_session_agent(...))` 로 백그라운드 invoke 발사.
- 백그라운드 task 는 lifespan 에서 1회 열어둔 LangGraph saver 를 재사용하고
agent.ainvoke → assistant MessageRow 영속 → 자동 compaction 까지 처리.
- 기존 SSE 스트림 (`/api/sessions/{id}/stream`) 이 새 메시지를 push,
프론트엔드의 `EventSource` 가 받아 thread 에 렌더.
신규 / 수정 파일:
- `static/conversation.html` (신규): chat UI 마크업. data-page="conversation".
- `static/app.js`: 새 페이지 핸들러 `bootstrapConversationPage` +
세션 picker + 메시지 thread 렌더 + SSE 구독 + Cmd/Ctrl+Enter 단축키.
XSS 정책 동일: 모든 사용자 콘텐츠는 `textContent` 만 사용.
- `static/style.css`: `.messages-thread`, `.msg-bubble`, `.conv-topbar`,
`.conv-input-bar` 등 chat UI 스타일.
- `api/app.py`: lifespan 에서 LangGraph saver 를 1회 열어 `app.state.saver`
에 보관 (Postgres 일 때만).
- `api/agent_runner.py` (신규): `invoke_session_agent(...)` — REPL 의
`InteractiveSession + _invoke_and_stream` 와 동일한 stack 을 HTTP background
context 용으로 재구성. 실패는 로깅 + return.
- `api/routes/sessions.py`: POST /messages 가 background task 발사 + ref 를
`app.state.pending_invocations` set 에 보관 (RUF006 / GC drop 방지).
테스트 (`tests/integration/test_conversation_gui.py`, 4 케이스):
- GET /conversation.html → 200 + 필수 마크업
- POST /messages → 200 + user row 영속 + 스텁 runner 호출 확인
- 백그라운드 task ref 가 `pending_invocations` 에 잡혀있고 완료 후 자동 discard
- 스텁 runner 가 assistant row 영속 → user + assistant 시퀀스 검증
게이트:
- ruff check / format --check / mypy: PASS
- pytest -q --ignore=tests/integration/test_e2e_workflow.py
--ignore=tests/integration/test_openrouter_smoke.py: 675 passed (4 신규 포함)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -778,3 +778,187 @@ select {
|
||||
.event-line { grid-template-columns: 1fr; gap: 2px; }
|
||||
.chips { grid-template-columns: 1fr; gap: 6px; }
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
v0.3 PR #8 — Conversation page
|
||||
================================================================= */
|
||||
|
||||
.conversation-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 80px);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.conv-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.conv-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.conv-picker {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
padding: 6px 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.conv-action-btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conv-action-btn:hover { filter: brightness(1.08); }
|
||||
|
||||
.conv-session-state {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.conv-session-state.state-active {
|
||||
background: rgba(34,197,94,0.12);
|
||||
color: rgb(22,163,74);
|
||||
}
|
||||
|
||||
.conv-session-state.state-ended {
|
||||
background: rgba(100,116,139,0.12);
|
||||
color: rgb(71,85,105);
|
||||
}
|
||||
|
||||
.messages-thread {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--bg);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.conv-empty {
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
padding: 40px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.msg-bubble {
|
||||
max-width: 80%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.msg-bubble.role-user {
|
||||
align-self: flex-end;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.msg-bubble.role-assistant {
|
||||
align-self: flex-start;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.msg-bubble.role-system {
|
||||
align-self: center;
|
||||
max-width: 90%;
|
||||
font-style: italic;
|
||||
font-size: 12.5px;
|
||||
background: rgba(245,158,11,0.08);
|
||||
border: 1px dashed rgba(245,158,11,0.4);
|
||||
color: rgb(120,53,15);
|
||||
}
|
||||
|
||||
.msg-bubble.pending {
|
||||
opacity: 0.6;
|
||||
font-size: 20px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
|
||||
.msg-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.msg-role {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.conv-input-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.conv-input-bar textarea {
|
||||
flex: 1;
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
resize: vertical;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.conv-input-bar textarea:disabled {
|
||||
background: var(--bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.conv-input-bar button {
|
||||
padding: 0 18px;
|
||||
font-size: 13px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conv-input-bar button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user