HAL 0.3.9: 로그인 터미널·텍스트 셸·모델 선택·슬래시 명령·점검 수정 — claude 실제 로그인 명령 auth login(대화형 세션·신뢰 설정도 허용, 작업 폴더 HOME), 텍스트 셸 범용화(순수 단말 에뮬레이터 term.mjs 로 TUI 렌더, 원시 키 전달·키 단추·붙여넣기·단말 크기), 유령 세션 한도 오류 수정(닫기=종료, 살아 있는 프로세스만 계산, 한도 3, 모두 종료·세션 목록), 모델 선택(확인된 목록·PATCH·/model), 슬래시 명령 20개(/help /status /order /deploy …, 원문 기록·확장 지시), 패널 로그인·재인증 단추, 개발 지시 v0.3.9(Oz)·docs/HAL-AI.md §8·VERSION_HISTORY 0.3.9 — test/hal-ai-039.test.mjs RED 0/6 → GREEN 6/6, 전체 28파일 195건 통과

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Claude Fable 5.1
2026-10-29 02:22:21 +00:00
parent f861c7ee5a
commit fe4800f94a
20 changed files with 506 additions and 49 deletions
+16 -2
View File
@@ -33,12 +33,12 @@ export function messageHtml(m, { actions = {} } = {}) {
export function statusLine(room, pending) {
if (!room) return '';
if (room.kind === 'group') return `<span class="muted small">통합 방 — 지시는 모든 활성 모델에게, <code>@이름</code> 으로 지정하면 그 모델만 답합니다. 모델→모델 전달(@이름)은 자동 1회만.</span>${pending ? ` <span class="badge running">응답 중 ${pending}</span>` : ''}`;
const c = room.connection || {}; return `${statusBadge(c.status)} <span class="muted small">${esc(c.name || '')} · ${esc(c.model || '')} · ${esc(c.provider || '')}</span>${pending ? ` <span class="badge running">응답 중</span>` : ''}`;
const c = room.connection || {}; return `${statusBadge(c.status)} <span class="muted small">${esc(c.name || '')} · ${esc(c.provider || '')}</span> ${modelSelectHtml({ id: c.id, model: c.model, models: c.models })}${c.status === 'reauth_required' ? ` <button class="btn small" data-act="ai-fix-login" data-id="${esc(c.id)}">로그인·재인증</button>` : ''}${pending ? ` <span class="badge running">응답 중</span>` : ''}`;
}
/** 연결 표(AI 연동 화면). 비밀값은 힌트만 온다. */
export function connectionsTable(conns) {
if (!conns.length) return `<div class="aw-state">연결된 AI 모델이 없습니다 — 아래 "연결 추가"로 API 키·OAuth·로그인(로컬 CLI) 방식 중 하나로 연결하세요</div>`;
const row = (c) => `<tr data-conn="${esc(c.id)}"><td data-l="이름"><b>${esc(c.name)}</b>${c.tabLabel ? `<br><span class="muted small">탭: ${esc(c.tabLabel)}</span>` : ''}<br><span class="muted small">${esc(c.identity?.role || '역할 없음')}${c.identity?.traits ? ' · ' + esc(c.identity.traits) : ''}</span></td><td data-l="공급자·인증">${esc(c.providerLabel)}<br><span class="muted small">${esc({ api_key: 'API 키', oauth: 'OAuth', login: '로그인(CLI)' }[c.authKind] || c.authKind)}${c.secret?.hint ? ` · ${esc(c.secret.hint)}` : ''}${c.secret?.expiresAt ? ` · 만료 ${esc(c.secret.expiresAt)}` : ''}${c.baseUrl ? `<br>${esc(c.baseUrl)}` : ''}${c.cli ? `<br>${esc(c.cli.tool || 'claude')}${c.cli.version ? ' ' + esc(c.cli.version) : ''}${c.cli.bin ? `<br><span class="mono">${esc(c.cli.bin)}</span>` : ' · 자동 탐색'}${c.cli.loggedIn === false ? ' · <span class="err">로그인 필요</span>' : c.cli.loggedIn === true ? ' · 로그인됨' : ''}` : ''}</span></td><td data-l="모델"><code>${esc(c.model || '')}</code>${c.models?.length ? `<br><span class="muted small">사용 가능 ${c.models.length}개</span>` : '<br><span class="muted small">확인 전</span>'}${c.tools ? `<br><span class="muted small">도구 ${esc(c.tools.preset)}(${c.tools.enabled.length})${c.toolsVia === 'mcp' ? ' · MCP' : ''}</span>` : ''}</td><td data-l="상태">${statusBadge(c.status)}<br><span class="muted small">${c.lastOkAt ? '마지막 확인 ' + esc(c.lastOkAt) : '확인 전'}</span>${c.lastError ? `<br><span class="err small">${esc(c.lastError)}</span>` : ''}</td><td data-l="조치"><div class="actions"><button class="btn small" data-act="ai-conn-test" data-id="${esc(c.id)}">확인</button>${c.authKind === 'oauth' ? `<button class="btn small" data-act="ai-conn-oauth" data-id="${esc(c.id)}">${c.status === 'active' ? '다시 로그인' : 'OAuth 로그인'}</button>` : ''}${c.authKind === 'api_key' ? `<button class="btn small" data-act="ai-conn-secret" data-id="${esc(c.id)}">키 교체</button>` : ''}<button class="btn small" data-act="ai-conn-edit" data-id="${esc(c.id)}">수정</button><button class="btn small" data-act="ai-conn-toggle" data-id="${esc(c.id)}" data-to="${c.status === 'disabled' ? 'active' : 'disabled'}">${c.status === 'disabled' ? '활성화' : '비활성화'}</button><button class="btn small danger" data-act="ai-conn-delete" data-id="${esc(c.id)}" data-name="${esc(c.name)}">삭제</button></div></td></tr>`;
const row = (c) => `<tr data-conn="${esc(c.id)}"><td data-l="이름"><b>${esc(c.name)}</b>${c.tabLabel ? `<br><span class="muted small">탭: ${esc(c.tabLabel)}</span>` : ''}<br><span class="muted small">${esc(c.identity?.role || '역할 없음')}${c.identity?.traits ? ' · ' + esc(c.identity.traits) : ''}</span></td><td data-l="공급자·인증">${esc(c.providerLabel)}<br><span class="muted small">${esc({ api_key: 'API 키', oauth: 'OAuth', login: '로그인(CLI)' }[c.authKind] || c.authKind)}${c.secret?.hint ? ` · ${esc(c.secret.hint)}` : ''}${c.secret?.expiresAt ? ` · 만료 ${esc(c.secret.expiresAt)}` : ''}${c.baseUrl ? `<br>${esc(c.baseUrl)}` : ''}${c.cli ? `<br>${esc(c.cli.tool || 'claude')}${c.cli.version ? ' ' + esc(c.cli.version) : ''}${c.cli.bin ? `<br><span class="mono">${esc(c.cli.bin)}</span>` : ' · 자동 탐색'}${c.cli.loggedIn === false ? ' · <span class="err">로그인 필요</span>' : c.cli.loggedIn === true ? ' · 로그인됨' : ''}` : ''}</span></td><td data-l="모델">${c.models?.length ? modelSelectHtml(c) : `<code>${esc(c.model || '')}</code><br><span class="muted small">확인 전</span>`}${c.tools ? `<br><span class="muted small">도구 ${esc(c.tools.preset)}(${c.tools.enabled.length})${c.toolsVia === 'mcp' ? ' · MCP' : ''}</span>` : ''}</td><td data-l="상태">${statusBadge(c.status)}<br><span class="muted small">${c.lastOkAt ? '마지막 확인 ' + esc(c.lastOkAt) : '확인 전'}</span>${c.lastError ? `<br><span class="err small">${esc(c.lastError)}</span>` : ''}</td><td data-l="조치"><div class="actions"><button class="btn small" data-act="ai-conn-test" data-id="${esc(c.id)}">확인</button>${c.authKind === 'oauth' ? `<button class="btn small" data-act="ai-conn-oauth" data-id="${esc(c.id)}">${c.status === 'active' ? '다시 로그인' : 'OAuth 로그인'}</button>` : ''}${c.authKind === 'api_key' ? `<button class="btn small" data-act="ai-conn-secret" data-id="${esc(c.id)}">키 교체</button>` : ''}<button class="btn small" data-act="ai-conn-edit" data-id="${esc(c.id)}">수정</button><button class="btn small" data-act="ai-conn-toggle" data-id="${esc(c.id)}" data-to="${c.status === 'disabled' ? 'active' : 'disabled'}">${c.status === 'disabled' ? '활성화' : '비활성화'}</button><button class="btn small danger" data-act="ai-conn-delete" data-id="${esc(c.id)}" data-name="${esc(c.name)}">삭제</button></div></td></tr>`;
return `<table class="r"><thead><tr><th>이름·역할</th><th>공급자·인증</th><th>모델</th><th>상태</th><th></th></tr></thead><tbody>${conns.map(row).join('')}</tbody></table>`;
}
@@ -69,3 +69,17 @@ export function toolPolicyHtml(tools, enabled) {
const on = new Set(enabled || []); const groups = [['read', '읽기(즉시 실행)'], ['write', '쓰기(승인·재인증)'], ['high', '고위험(승인·재인증·3단계 확인)']];
return groups.map(([risk, label]) => { const list = tools.filter((t) => t.risk === risk); return `<div class="ai-tool-group"><b>${esc(label)}</b> <button type="button" class="btn small" data-act="ai-tools-group" data-risk="${risk}" data-on="1">모두 켜기</button> <button type="button" class="btn small" data-act="ai-tools-group" data-risk="${risk}" data-on="0">모두 끄기</button><div class="ai-tool-list">${list.map((t) => `<label class="small" title="${esc(t.description)}"><input type="checkbox" name="toolsEnabled" value="${esc(t.name)}" data-risk="${risk}"${on.has(t.name) ? ' checked' : ''}> ${esc(t.name)}</label>`).join('')}</div></div>`; }).join('');
}
/** [0.3.9] 모델 선택(확인된 목록에서). 목록이 없으면 "확인" 안내. */
export function modelSelectHtml(c) {
const models = (c && c.models) || []; if (!models.length) return `<span class="muted small">모델 목록 확인 전 — "확인"을 누르면 고를 수 있습니다</span>`;
const cur = c.model || ''; const opts = [...new Set([cur, ...models].filter(Boolean))];
return `<select class="ai-model-pick" data-act="ai-model-pick" data-id="${esc(c.id)}" aria-label="모델 선택">${opts.map((m) => `<option value="${esc(m)}"${m === cur ? ' selected' : ''}>${esc(m)}${!models.includes(m) ? ' (목록 밖)' : ''}</option>`).join('')}</select>`;
}
/** [0.3.9] 텍스트 셸 세션 목록(살아 있는 것·끝난 것 구분, 열기·종료) */
export function shellSessionsHtml(sessions) {
if (!sessions || !sessions.length) return '<span class="muted small">열린 텍스트 셸이 없습니다.</span>';
return `<table class="r"><thead><tr><th>세션</th><th>상태</th><th>시작</th><th></th></tr></thead><tbody>${sessions.map((s) => `<tr><td data-l="세션"><b>${esc(s.tool)} ${esc(s.command)}</b> <span class="muted small mono">${esc(s.id)}</span></td><td data-l="상태">${s.status === 'running' ? '<span class="badge running">실행 중</span>' : `<span class="badge ${s.exitCode === 0 ? 'ok' : 'neutral'}">${esc(s.status)}${s.exitCode != null ? `(${s.exitCode})` : ''}</span>`}</td><td data-l="시작"><span class="muted small">${esc(fmtTime(s.startedAt))}</span></td><td data-l=""><div class="actions">${s.status === 'running' ? `<button class="btn small" data-act="ai-shell-attach" data-id="${esc(s.id)}" data-tool="${esc(s.tool)}" data-command="${esc(s.command)}">열기</button><button class="btn small danger" data-act="ai-shell-kill-id" data-id="${esc(s.id)}">종료</button>` : `<button class="btn small" data-act="ai-shell-attach" data-id="${esc(s.id)}" data-tool="${esc(s.tool)}" data-command="${esc(s.command)}">출력 보기</button>`}</div></td></tr>`).join('')}</tbody></table>`;
}
/** [0.3.9] /help 명령 목록 */
export function commandsHelpHtml(commands) { return `<table class="r"><thead><tr><th>명령</th><th>설명</th></tr></thead><tbody>${(commands || []).map((c) => `<tr><td data-l="명령"><code>${esc(c.usage)}</code></td><td data-l="설명">${esc(c.description)}${c.kind === 'local' ? ' <span class="muted small">(HAL 즉시)</span>' : ''}</td></tr>`).join('')}</tbody></table>`; }