2026-10-29 02:49:46 +00:00
/* hal/ui/public/ai.js — 운영자 콘솔 AI 채팅룸 패널·AI 연동 화면 브라우저 스크립트(0.3.0, 개발 지시 v0.3.0 · 0.3.1 서버 설치 CLI 자동 탐색 카드 · 0.3.2 사전 안내·OAuth 자동 진행·텍스트 셸·도구 정책·MCP 설정 · 0.3.9 단말 에뮬레이터 셸(원시 키)·세션 목록·모델 선택·슬래시 명령 · 0.3.10 셸 복사(선택 Ctrl+C·감지된 주소·전체 복사·텍스트 보기)).
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
* - 패널(#ai-panel): 탭(통합 고정 + 연결별), 대화 기록, 지시 입력, 제안 승인·거부. 열림/너비/선택 탭은 운영자별 localStorage 에 두고 새로 고침 뒤 복원한다.
* - 데이터는 /ops/api/ai/* 만 쓴다(CSRF 자동). 응답 대기 중에는 1초, 아니면 4초 간격으로 다시 읽는다(탭이 숨겨지면 멈춤). 화면을 떠나도 턴은 HAL 이 계속한다.
* - 제안 승인: 쓰기 → 재인증 창(서버가 reauth_required 로 거부한 그 때만, reauth.mjs 계약), 고위험 → 3단계 확인(영향 범위 → 대상 식별정보 재입력 → 최종 실행) 뒤 승인 API.
* 승인 판단·실행은 전부 서버가 한다(여기는 묻고 보내는 것뿐). 비밀값은 입력란에서 서버로만 가고 화면에 다시 그리지 않는다.
* - 렌더링은 ai-render.mjs(순수)가 맡아 Node 테스트가 같은 HTML 계약을 검사한다.
*/
import * as A from './ai-render.mjs' ;
import { callWithReauth , reauthPrompt } from './reauth.mjs' ;
2026-10-29 02:49:46 +00:00
import { Term , renderHtml , keyToBytes , extractUrls , plainText , copyDecision } from './term.mjs' ;
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
const $ = ( s , el = document ) => el . querySelector ( s );
const csrf = () => ( $ ( 'meta[name=csrf]' ) || {}). content || '' ;
async function api ( method , path , body ) {
const r = await fetch ( path , { method , headers : { 'Content-Type' : 'application/json' , 'X-CSRF' : csrf () }, body : body === undefined ? undefined : JSON . stringify ( body ), credentials : 'same-origin' });
let j = null ; try { j = await r . json (); } catch ( _ ) {}
if ( ! r . ok || ( j && j . ok === false )) { const e = new Error (( j && j . message ) || `오류 ${ r . status } ` ); e . code = ( j && j . error ) || 'http_' + r . status ; e . detail = j && j . detail ; e . status = r . status ; throw e ; }
return j ;
}
let toastT ; function toast ( t ) { const el = $ ( '#toast' ); if ( ! el ) return alert ( t ); el . textContent = t ; el . classList . add ( 'show' ); clearTimeout ( toastT ); toastT = setTimeout (() => el . classList . remove ( 'show' ), 5000 ); }
function modal ( html ) { const m = $ ( '#modal' ); m . innerHTML = `<div class="box" role="dialog" aria-modal="true"> ${ html } </div>` ; m . hidden = false ; const close = () => { m . hidden = true ; m . innerHTML = '' ; }; m . onclick = ( e ) => { if ( e . target === m ) close (); }; return { el : m , close }; }
/** 입력 창(ops.js 의 ask 와 같은 계약의 축소판). fields: [{name,label,type,required,value,hint}] → 값 객체 또는 null */
function ask ( title , fields , { okLabel = '확인' , danger = false , note = '' } = {}) {
return new Promise (( resolve ) => {
const m = modal ( `<h2> ${ A . esc ( title ) } </h2> ${ note ? `<p class="muted"> ${ note } </p>` : '' } <form id="aiaskf"> ${ fields . map (( f ) => f . type === 'textarea' ? `<label> ${ A . esc ( f . label ) } <textarea name=" ${ f . name } " rows="3"> ${ A . esc ( f . value || '' ) } </textarea></label>` : f . type === 'checkbox' ? `<label><input type="checkbox" name=" ${ f . name } "> ${ A . esc ( f . label ) } </label>` : `<label> ${ A . esc ( f . label ) } <input name=" ${ f . name } " type=" ${ f . type || 'text' } " value=" ${ A . esc ( f . value || '' ) } " ${ f . required ? ' required' : '' } autocomplete=" ${ f . type === 'password' ? 'new-password' : 'off' } "></label>` ). join ( '' ) }${ fields . some (( f ) => f . hint ) ? `<p class="muted small"> ${ fields . filter (( f ) => f . hint ). map (( f ) => A . esc ( f . hint )). join ( '<br>' ) } </p>` : '' } <div class="actions"><button type="button" class="btn" id="aiaskc">취소</button><button class="btn ${ danger ? 'danger' : 'primary' } "> ${ A . esc ( okLabel ) } </button></div></form>` );
$ ( '#aiaskc' , m . el ). onclick = () => { m . close (); resolve ( null ); };
$ ( '#aiaskf' , m . el ). onsubmit = ( e ) => { e . preventDefault (); const fd = new FormData ( e . target ); const out = {}; for ( const f of fields ) out [ f . name ] = fd . get ( f . name ); m . close (); resolve ( out ); };
});
}
/** 재인증 창 — 서버가 reauth_required 로 거부한 그 때만 고위험 안내가 붙는다(reauth.mjs). */
async function reauth ( opts = {}) { const p = reauthPrompt ( opts ); const v = await ask ( p . title , [{ name : 'password' , label : '운영자 비밀번호' , type : 'password' , required : true }], { okLabel : p . okLabel , note : p . note ? A . esc ( p . note ) : '' }); if ( ! v ) return false ; try { await api ( 'POST' , '/ops/api/reauth' , { password : v . password }); toast ( '재인증했습니다' ); return true ; } catch ( e ) { toast ( e . message ); return false ; } }
const call = ( method , path , body ) => callWithReauth (() => api ( method , path , body ), reauth );
/** 고위험 제안 승인용 3단계 확인(ops.js threeStep 과 같은 계약: confirmImpact → confirmIdentifiers → confirmExecute) */
function threeStep ({ title , impacts = [], identifiers = [], revertible = '되돌리기 가능 여부를 확인하세요' }) {
return new Promise (( resolve ) => {
let step = 1 ; const m = modal ( '' );
const render = () => { m . el . innerHTML = `<div class="box" role="dialog" aria-modal="true"><h2> ${ A . esc ( title ) } — ${ step } /3단계</h2> ${ step === 1 ? `<p><b>1단계 영향 범위 확인</b></p><ul> ${ impacts . map (( i ) => `<li> ${ A . esc ( i ) } </li>` ). join ( '' ) } </ul><label><input type="checkbox" id="c1"> 위 영향 범위를 확인했습니다</label>` : step === 2 ? `<p><b>2단계 대상 식별정보 재확인</b> — 아래 식별정보를 정확히 입력하세요: <code> ${ identifiers . map ( A . esc ). join ( ', ' ) } </code></p><input id="c2" placeholder="식별정보(쉼표로 구분)" autocomplete="off">` : `<p><b>3단계 최종 실행</b></p><p> ${ A . esc ( revertible ) } </p><label><input type="checkbox" id="c3"> 최종 실행을 확인합니다</label>` } <div class="actions"><button class="btn" id="cancel">취소</button><button class="btn ${ step === 3 ? 'danger' : 'primary' } " id="next"> ${ step === 3 ? '실행' : '다음' } </button></div></div>` ;
$ ( '#cancel' , m . el ). onclick = () => { m . close (); resolve ( null ); };
$ ( '#next' , m . el ). onclick = () => { if ( step === 1 && ! $ ( '#c1' , m . el ). checked ) return toast ( '영향 범위를 확인해야 합니다' ); if ( step === 2 ) { const got = $ ( '#c2' , m . el ). value . split ( ',' ). map (( s ) => s . trim ()). filter ( Boolean ). sort (); if ( JSON . stringify ( got ) !== JSON . stringify ( identifiers . slice (). sort ())) return toast ( '대상 식별정보가 다릅니다' ); } if ( step === 3 && ! $ ( '#c3' , m . el ). checked ) return toast ( '최종 실행을 확인해야 합니다' ); if ( step < 3 ) { step ++ ; render (); } else { m . close (); resolve ({ confirmImpact : true , confirmIdentifiers : identifiers , confirmExecute : true }); } }; };
render ();
});
}
// ---------------- 패널 상태(운영자별 localStorage)
const panel = $ ( '#ai-panel' ); const operator = panel ? panel . dataset . operator || '' : '' ;
const KEY = `aw_ai_panel: ${ operator } ` ;
const loadState = () => { try { return JSON . parse ( localStorage . getItem ( KEY ) || '{}' ) || {}; } catch ( _ ) { return {}; } };
const saveState = ( p ) => { try { localStorage . setItem ( KEY , JSON . stringify ({ ... loadState (), ... p })); } catch ( _ ) {} };
let rooms = []; let current = null ; let lastSeq = 0 ; let actions = {}; let pollT = null ; let msgs = [];
function applyState () { const st = loadState (); if ( ! panel ) return ; panel . classList . toggle ( 'closed' , st . open === false ); if ( st . width ) panel . style . setProperty ( '--ai-w' , Math . max ( 280 , Math . min ( window . innerWidth * 0.6 , st . width )) + 'px' ); const b = $ ( '[data-act=ai-toggle]' ); if ( b ) b . setAttribute ( 'aria-pressed' , String ( st . open !== false )); }
function pageContext () { const main = document . querySelector ( '[data-page]' ); return main ? { page : main . dataset . page || '' , id : main . dataset . id || '' } : null ; }
async function loadRooms () {
const r = await api ( 'GET' , '/ops/api/ai/rooms' ); rooms = r . rooms ; const st = loadState ();
if ( ! current || ! rooms . some (( x ) => x . id === current )) current = rooms . some (( x ) => x . id === st . room ) ? st . room : 'room_group' ;
$ ( '#ai-tabs' ). innerHTML = A . tabsHtml ( rooms , current );
}
function room () { return rooms . find (( x ) => x . id === current ) || null ; }
async function loadMessages ({ full = false } = {}) {
if ( ! current ) return ; const r = await api ( 'GET' , `/ops/api/ai/rooms/ ${ encodeURIComponent ( current ) } /messages ${ full ? '' : '?after=' + lastSeq } ` );
actions = {}; for ( const a of r . actions || []) actions [ a . id ] = a ;
if ( full ) { msgs = r . messages ; } else if ( r . messages . length ) { msgs = msgs . concat ( r . messages ); }
lastSeq = msgs . length ? msgs [ msgs . length - 1 ]. seq : 0 ;
const log = $ ( '#ai-log' ); const atBottom = log . scrollHeight - log . scrollTop - log . clientHeight < 60 ;
log . innerHTML = msgs . length ? msgs . map (( m ) => A . messageHtml ( m , { actions })). join ( '' ) : `<div class="aw-state"> ${ room () && room (). kind === 'group' ? '통합 방입니다. 연결된 모든 활성 모델이 함께 듣습니다.' : '아직 대화가 없습니다.' } </div>` ;
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
const proposed = ( r . actions || []). filter (( a ) => a . status === 'proposed' ). length ; $ ( '#ai-status' ). innerHTML = A . statusLine ( room (), r . pending ) + ( proposed ? ` <span class="badge warn">승인 대기 ${ proposed } </span>` : '' );
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
if ( atBottom || full ) log . scrollTop = log . scrollHeight ;
schedule ( r . pending ? 1000 : 4000 );
}
function schedule ( ms ) { clearTimeout ( pollT ); pollT = setTimeout (() => { if ( document . hidden || ! panel || panel . classList . contains ( 'closed' )) return schedule ( 4000 ); loadMessages (). catch (() => schedule ( 6000 )); }, ms ); }
async function switchRoom ( id ) { current = id ; lastSeq = 0 ; msgs = []; saveState ({ room : id }); $ ( '#ai-tabs' ). innerHTML = A . tabsHtml ( rooms , current ); await loadMessages ({ full : true }); }
async function send () {
const ta = $ ( '#ai-input' ); const text = ta . value . trim (); if ( ! text || ! current ) return ;
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건 통과
2026-10-29 02:22:21 +00:00
if ( text === '/' ) { ta . value = '/help' ; }
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
try { ta . disabled = true ; await call ( 'POST' , `/ops/api/ai/rooms/ ${ encodeURIComponent ( current ) } /messages` , { text , context : $ ( '#ai-ctx' ) ? . checked ? pageContext () : null }); ta . value = '' ; await loadMessages (); }
catch ( e ) { toast ( e . code === 'unknown_addressee' ? ` ${ e . message }${ e . detail && e . detail . participants ? ' — 참여자: ' + e . detail . participants . join ( ', ' ) : '' } ` : e . message ); }
finally { ta . disabled = false ; ta . focus (); }
}
async function approve ( id , risk , ids , tool ) {
const a = actions [ id ] || { args : {}, description : '' };
let body = {};
if ( risk === 'high' ) { body = await threeStep ({ title : `AI 제안 승인: ${ tool } ` , impacts : [ a . description || tool , `인자: ${ JSON . stringify ( a . args || { } )}` , '이 조치는 고위험으로 분류되어 재인증과 3단계 확인이 필요합니다' ], identifiers : ids , revertible : '되돌리기 가능 여부는 도구 설명·HAL 작업 결과로 확인하세요' }); if ( ! body ) return ; }
else { const v = await ask ( `AI 제안 승인: ${ tool } ` , [{ name : 'ok' , label : ` ${ a . description || tool } — 인자 ${ JSON . stringify ( a . args || { } )}` , type : 'checkbox' }], { okLabel : '승인(재인증 필요)' }); if ( ! v || v . ok !== 'on' ) return toast ( '확인란을 체크해야 승인됩니다' ); }
await call ( 'POST' , `/ops/api/ai/actions/ ${ encodeURIComponent ( id ) } /approve` , body ); toast ( '승인했습니다 — 실행 결과가 대화에 들어갑니다' ); await loadMessages ({ full : true });
}
async function reject ( id ) { const v = await ask ( 'AI 제안 거부' , [{ name : 'reason' , label : '사유(선택)' }], { okLabel : '거부' , danger : true }); if ( ! v ) return ; await api ( 'POST' , `/ops/api/ai/actions/ ${ encodeURIComponent ( id ) } /reject` , { reason : v . reason || '' }); toast ( '거부했습니다 — 실행되지 않습니다' ); await loadMessages ({ full : true }); }
// ---------------- AI 연동 화면(/ops/ai)
const PAGE_AI = document . querySelector ( '[data-page="ai"]' );
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
let conns = []; let providers = []; let callbackUrl = '' ; let toolList = [];
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
const f = () => $ ( '#ai-conn-form' );
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
const providerOf = ( id ) => providers . find (( p ) => p . id === id ) || null ;
function renderGuide () { const form = f (); if ( ! form || ! $ ( '#ai-guide' )) return ; $ ( '#ai-guide' ). innerHTML = A . guideHtml ( providerOf ( form . provider . value ), form . authKind . value , callbackUrl ); }
function renderToolPolicy () { const form = f (); if ( ! form ) return ; const box = $ ( '#ai-tool-policy' ); box . hidden = form . toolsPreset . value !== 'custom' ; if ( ! box . hidden && ! box . dataset . ready ) { box . innerHTML = A . toolPolicyHtml ( toolList , toolList . map (( t ) => t . name )); box . dataset . ready = '1' ; } }
function toolsBody () { const form = f (); const preset = form . toolsPreset . value ; if ( preset !== 'custom' ) return { preset }; return { preset : 'custom' , enabled : [... form . querySelectorAll ( 'input[name=toolsEnabled]:checked' )]. map (( x ) => x . value ) }; }
// ---------------- [0.3.2] OAuth 자동 진행: 저장 직후 로그인 창(팝업)을 열고, 완료 페이지가 postMessage 로 알리면 확인까지 잇는다
let oauthWin = null ;
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건 통과
2026-10-29 02:22:21 +00:00
async function startOauth ( id ) { const r = await api ( 'POST' , `/ops/api/ai/connections/ ${ id } /oauth/start` ); toast ( '공급자 로그인 창을 엽니다 — 승인하면 자동으로 돌아옵니다' ); oauthWin = window . open ( r . authorizeUrl , 'hal-oauth' , 'width=600,height=760' ); if ( ! oauthWin ) { toast ( '팝업이 막혀 같은 창에서 이동합니다' ); location . href = r . authorizeUrl ; } }
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
window . addEventListener ( 'message' , async ( e ) => { if ( e . origin !== location . origin || ! e . data || e . data . type !== 'hal-oauth' ) return ; try { oauthWin && oauthWin . close (); } catch ( _ ) {} if ( e . data . ok ) { toast ( 'OAuth 로그인 완료 — 토큰을 Vault 에 저장했습니다. 모델 목록을 확인합니다' ); if ( e . data . connection ) { try { await api ( 'POST' , `/ops/api/ai/connections/ ${ e . data . connection } /test` ); } catch ( err ) { toast ( `확인 실패: ${ err . message } ` ); } } } else toast ( `OAuth 로그인 실패: ${ e . data . reason || '거부됨' } ` ); await loadConns (); });
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건 통과
2026-10-29 02:22:21 +00:00
// ---------------- [0.3.2 → 0.3.9] 텍스트 셸: 단말 에뮬레이터(term.mjs)로 TUI 를 그리고, 단말 영역에 포커스하면 키를 바이트 그대로 보낸다(화살표·Enter·Esc·Ctrl). 줄 입력란·키 단추도 제공. 닫기는 세션을 종료한다.
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
let shell = null ; let shellT = null ; let shellM = null ;
2026-10-29 02:49:46 +00:00
/** [0.3.10] 클립보드 복사: Clipboard API → 실패하면 숨은 textarea + execCommand. 긴 주소도 통째로. */
async function copyText ( t ) { if ( ! t ) return toast ( '복사할 내용이 없습니다' ); try { await navigator . clipboard . writeText ( t ); toast ( `복사했습니다( ${ t . length } 자)` ); return ; } catch ( _ ) {} try { const ta = document . createElement ( 'textarea' ); ta . value = t ; ta . setAttribute ( 'readonly' , '' ); ta . style . position = 'fixed' ; ta . style . left = '-9999px' ; document . body . appendChild ( ta ); ta . select (); const ok = document . execCommand ( 'copy' ); ta . remove (); toast ( ok ? `복사했습니다( ${ t . length } 자)` : '복사에 실패했습니다 — "텍스트로 보기"에서 직접 선택하세요' ); } catch ( _ ) { toast ( '복사에 실패했습니다 — "텍스트로 보기"에서 직접 선택하세요' ); } }
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건 통과
2026-10-29 02:22:21 +00:00
const COLS = 100 , ROWS = 30 ;
async function openShell ( tool , command = 'login' , attach = null ) {
let r ; if ( attach ) r = { session : attach , guide : [] }; else r = await call ( 'POST' , '/ops/api/ai/shell' , { tool , command , cols : COLS , rows : ROWS });
shell = { id : r . session . id , next : 0 , term : new Term ( r . session . cols || COLS , r . session . rows || ROWS ), session : r . session };
2026-10-29 02:49:46 +00:00
shellM = modal ( A . shellModalHtml ( r . session , r . guide || []));
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건 통과
2026-10-29 02:22:21 +00:00
const sendBytes = ( b ) => api ( 'POST' , `/ops/api/ai/shell/ ${ shell . id } /input` , { data : b }). catch (( err ) => toast ( err . message ));
const termEl = $ ( '#ai-term' , shellM . el );
2026-10-29 02:49:46 +00:00
// [0.3.10] 복사 대응: 선택이 있으면 Ctrl+C 는 브라우저 복사(기본 동작)에 맡기고, Ctrl+Shift+C·Ctrl+Insert 는 항상 복사, Ctrl+V 는 붙여넣기(붙여넣기 이벤트가 처리)
const selectedText = () => { const sel = window . getSelection ? window . getSelection () : null ; return sel && ! sel . isCollapsed && termEl . contains ( sel . anchorNode ) ? sel . toString () : '' ; };
termEl . addEventListener ( 'keydown' , ( e ) => { const d = copyDecision ( e , !! selectedText ()); if ( d === 'copy' ) { if ( e . shiftKey || e . key === 'Insert' ) { e . preventDefault (); copyText ( selectedText () || plainText ( shell . term )); } return ; } if ( d === 'paste' ) return ; const b = keyToBytes ( e ); if ( b == null ) return ; e . preventDefault (); if ( shell && shell . session . status === 'running' ) sendBytes ( b ); });
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건 통과
2026-10-29 02:22:21 +00:00
termEl . addEventListener ( 'paste' , ( e ) => { const t = ( e . clipboardData || window . clipboardData ). getData ( 'text' ); if ( t ) { e . preventDefault (); sendBytes ( t ); } });
for ( const b of shellM . el . querySelectorAll ( '[data-key]' )) b . onclick = () => { sendBytes ( b . dataset . key ); termEl . focus (); };
2026-10-29 02:49:46 +00:00
shellM . el . addEventListener ( 'click' , async ( e ) => { const el = e . target . closest ( '[data-act]' ); if ( ! el ) return ; const act = el . dataset . act ;
if ( act === 'ai-shell-copy-url' ) { e . preventDefault (); copyText ( el . dataset . url ); }
else if ( act === 'ai-shell-copy-all' ) { e . preventDefault (); copyText ( plainText ( shell . term )); }
else if ( act === 'ai-shell-copy-sel' ) { e . preventDefault (); const t = selectedText (); if ( ! t ) return toast ( '단말에서 글을 먼저 드래그로 선택하세요' ); copyText ( t ); }
else if ( act === 'ai-shell-text' ) { e . preventDefault (); const tv = $ ( '#ai-shell-textview' , shellM . el ); tv . hidden = ! tv . hidden ; if ( ! tv . hidden ) { tv . value = plainText ( shell . term ); tv . focus (); tv . select (); } } });
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건 통과
2026-10-29 02:22:21 +00:00
$ ( '#ai-shell-form' , shellM . el ). onsubmit = async ( e ) => { e . preventDefault (); const inp = $ ( '#ai-shell-in' , shellM . el ); const v = inp . value ; inp . value = '' ; await sendBytes ( v + '\r' ); };
const closeShell = async () => { clearTimeout ( shellT ); if ( shell && shell . session . status === 'running' ) { try { await api ( 'DELETE' , `/ops/api/ai/shell/ ${ shell . id } ` ); } catch ( _ ) {} } shellM . close (); shell = null ; loadShells (); loadClis (); loadConns (); };
shellM . el . querySelector ( '[data-act=ai-shell-close]' ). onclick = closeShell ; shellM . el . querySelector ( '[data-act=ai-shell-kill]' ). onclick = () => api ( 'DELETE' , `/ops/api/ai/shell/ ${ shell . id } ` ). catch (( err ) => toast ( err . message ));
shellM . el . onclick = ( e ) => { if ( e . target === shellM . el ) closeShell (); };
termEl . focus (); pollShell ();
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
}
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건 통과
2026-10-29 02:22:21 +00:00
async function pollShell () {
if ( ! shell ) return ;
try { const r = await api ( 'GET' , `/ops/api/ai/shell/ ${ shell . id } ?after= ${ shell . next } ` ); if ( r . output ) shell . term . feed ( r . output ); shell . next = r . next ; shell . session = r . session ; const view = $ ( '#ai-shell-view' ); if ( view ) view . innerHTML = A . shellHtml ( r . session , '' ); const el = $ ( '#ai-term' ); if ( el ) { el . innerHTML = renderHtml ( shell . term ); el . scrollTop = el . scrollHeight ; }
2026-10-29 02:49:46 +00:00
const urls = extractUrls ( shell . term ); const ul = $ ( '#ai-shell-urls' ); if ( ul && ul . dataset . sig !== urls . join ( '|' )) { ul . dataset . sig = urls . join ( '|' ); ul . innerHTML = A . shellUrlsHtml ( urls ); } const tv = $ ( '#ai-shell-textview' ); if ( tv && ! tv . hidden && document . activeElement !== tv ) tv . value = plainText ( shell . term ); // [0.3.10] 감지된 주소·텍스트 보기 갱신
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건 통과
2026-10-29 02:22:21 +00:00
if ( r . session . status === 'running' ) shellT = setTimeout ( pollShell , 300 ); else { toast ( `셸 종료(코드 ${ r . session . exitCode } ) — 로그인 상태를 다시 탐색합니다` ); loadShells (); loadClis (); loadConns (); } }
catch ( e ) { toast ( e . message ); }
}
let shellSessions = [];
async function loadShells () { const el = $ ( '#ai-shells' ); if ( ! el ) return ; try { const r = await api ( 'GET' , '/ops/api/ai/shell' ); shellSessions = r . sessions ; el . innerHTML = A . shellSessionsHtml ( r . sessions ); } catch ( e ) { el . innerHTML = `<div class="aw-state failed"> ${ A . esc ( e . message ) } </div>` ; } }
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
async function loadMcp () { try { const r = await api ( 'GET' , '/ops/api/ai/mcp-config' ); $ ( '#ai-mcp' ). innerHTML = `<p class="small">Claude Code: <code> ${ A . esc ( r . claudeCommand ) } </code></p><pre class="pre small"> ${ A . esc ( JSON . stringify ( r . claude , null , 1 )) } </pre><p class="small">Codex(<code>~/.codex/config.toml</code>):</p><pre class="pre small"> ${ A . esc ( r . codexToml ) } </pre><p class="muted small"> ${ A . esc ( r . note ) } </p>` ; } catch ( e ) { $ ( '#ai-mcp' ). innerHTML = `<div class="aw-state failed"> ${ A . esc ( e . message ) } </div>` ; } }
function syncAuthFields () { const form = f (); if ( ! form ) return ; const prov = form . provider . value ; const kinds = ( providerOf ( prov ) || {}). authKinds || ({ anthropic : [ 'api_key' , 'oauth' ], openai : [ 'api_key' , 'oauth' ], gemini : [ 'api_key' , 'oauth' ], openrouter : [ 'api_key' , 'oauth' ], other : [ 'api_key' , 'oauth' ], 'local-cli' : [ 'login' ] }[ prov ] || [ 'api_key' ]); for ( const o of form . authKind . options ) o . hidden = ! kinds . includes ( o . value ); if ( ! kinds . includes ( form . authKind . value )) form . authKind . value = kinds [ 0 ]; for ( const fs of form . querySelectorAll ( 'fieldset[data-auth]' )) fs . hidden = fs . dataset . auth !== form . authKind . value ; renderGuide (); }
async function loadConns () { const r = await api ( 'GET' , '/ops/api/ai/connections' ); conns = r . connections ; providers = r . providers || providers ; callbackUrl = r . callbackUrl || callbackUrl ; $ ( '#ai-conns' ). innerHTML = A . connectionsTable ( conns ); renderGuide (); }
2026-10-29 01:11:32 +00:00
async function loadClis () { try { const r = await api ( 'GET' , '/ops/api/ai/cli' ); $ ( '#ai-clis' ). innerHTML = A . clisHtml ( r ); } catch ( e ) { $ ( '#ai-clis' ). innerHTML = `<div class="aw-state failed"> ${ A . esc ( e . message ) } </div>` ; } }
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
async function loadTools () { try { const r = await api ( 'GET' , '/ops/api/ai/tools' ); toolList = r . tools ; $ ( '#ai-tools' ). innerHTML = `<table class="r"><thead><tr><th>도구</th><th>위험 등급</th><th>설명</th></tr></thead><tbody> ${ r . tools . map (( t ) => `<tr><td data-l="도구"><code> ${ A . esc ( t . name ) } </code></td><td data-l="위험 등급"><span class="badge ${ t . risk === 'read' ? 'ok' : t . risk === 'write' ? 'warn' : 'failed' } "> ${ A . esc ( t . riskLabel ) } </span></td><td data-l="설명"> ${ A . esc ( t . description ) } </td></tr>` ). join ( '' ) } </tbody></table>` ; } catch ( e ) { $ ( '#ai-tools' ). innerHTML = `<div class="aw-state failed"> ${ A . esc ( e . message ) } </div>` ; } }
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
function fillForm ( c ) { const form = f (); form . id . value = c ? c . id : '' ; form . name . value = c ? c . name : '' ; form . provider . value = c ? c . provider : 'anthropic' ; form . authKind . value = c ? c . authKind : 'api_key' ; form . model . value = c ? c . model || '' : '' ; form . baseUrl . value = c ? c . baseUrl || '' : '' ; form . apiKey . value = '' ; form . oauthClientSecret . value = '' ;
2026-10-29 01:11:32 +00:00
form . oauthAuthorizeUrl . value = c && c . oauth ? c . oauth . authorizeUrl : '' ; form . oauthTokenUrl . value = c && c . oauth ? c . oauth . tokenUrl : '' ; form . oauthClientId . value = c && c . oauth ? c . oauth . clientId : '' ; form . oauthScopes . value = c && c . oauth ? ( c . oauth . scopes || []). join ( ' ' ) : '' ; form . cliBin . value = c && c . cli ? c . cli . bin || '' : '' ; form . cliTool . value = c && c . cli && c . cli . tool ? c . cli . tool : 'claude' ;
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
form . role . value = c ? c . identity ? . role || '' : '' ; form . traits . value = c ? c . identity ? . traits || '' : '' ; form . instructions . value = c ? c . identity ? . instructions || '' : '' ;
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
form . toolsPreset . value = c && c . tools ? c . tools . preset : 'all' ; const box = $ ( '#ai-tool-policy' ); box . dataset . ready = '' ; if ( form . toolsPreset . value === 'custom' ) { box . innerHTML = A . toolPolicyHtml ( toolList , c . tools . enabled ); box . dataset . ready = '1' ; } renderToolPolicy ();
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
form . provider . disabled = !! c ; form . authKind . disabled = !! c ; $ ( '#ai-conn-submit' ). textContent = c ? '수정 저장' : '연결 추가' ; $ ( '[data-act=ai-conn-cancel]' , form ). hidden = ! c ; $ ( '#ai-conn-msg' ). textContent = '' ; syncAuthFields (); if ( c ) form . scrollIntoView ({ behavior : 'smooth' }); }
async function submitConn ( e ) {
e . preventDefault (); const form = f (); const fd = new FormData ( form ); const id = fd . get ( 'id' ); const msg = $ ( '#ai-conn-msg' ); msg . textContent = '' ;
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
const body = { name : fd . get ( 'name' ), model : fd . get ( 'model' ) || undefined , baseUrl : fd . get ( 'baseUrl' ) || undefined , identity : { role : fd . get ( 'role' ) || '' , traits : fd . get ( 'traits' ) || '' , instructions : fd . get ( 'instructions' ) || '' }, tools : toolsBody () };
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
if ( ! id ) { body . provider = form . provider . value ; body . authKind = form . authKind . value ; }
const kind = form . authKind . value ;
if ( kind === 'api_key' && fd . get ( 'apiKey' )) body . apiKey = fd . get ( 'apiKey' );
if ( kind === 'oauth' ) { body . oauth = { authorizeUrl : fd . get ( 'oauthAuthorizeUrl' ), tokenUrl : fd . get ( 'oauthTokenUrl' ), clientId : fd . get ( 'oauthClientId' ), scopes : String ( fd . get ( 'oauthScopes' ) || '' ). split ( /\s+/ ). filter ( Boolean ) }; if ( fd . get ( 'oauthClientSecret' )) body . oauth . clientSecret = fd . get ( 'oauthClientSecret' ); }
2026-10-29 01:11:32 +00:00
if ( kind === 'login' ) body . cli = { tool : fd . get ( 'cliTool' ) || 'claude' , bin : fd . get ( 'cliBin' ) || undefined };
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
try {
if ( id ) { delete body . apiKey ; await call ( 'PATCH' , `/ops/api/ai/connections/ ${ encodeURIComponent ( id ) } ` , body ); toast ( '수정했습니다' ); fillForm ( null ); form . reset (); syncAuthFields (); await loadConns (); return ; }
const r = await call ( 'POST' , '/ops/api/ai/connections' , body ); fillForm ( null ); form . reset (); syncAuthFields (); form . apiKey . value = '' ; form . oauthClientSecret . value = '' ; await loadConns ();
// [0.3.2] 다음 단계를 자동으로 진행: OAuth → 로그인 창, CLI → 탐색·로그인 확인(안 되어 있으면 텍스트 셸), API 키 → 모델 확인
const next = r . next || {}; const cid = r . connection . id ; if ( next . note ) toast ( next . note );
if ( next . action === 'oauth' ) await startOauth ( cid );
else if ( next . action === 'cli_check' ) { try { const t = await api ( 'POST' , `/ops/api/ai/connections/ ${ cid } /test` ); toast ( `CLI 확인됨 — ${ t . cliVersion || '' } · 로그인 ${ t . loggedIn === false ? '필요' : '됨' } ` ); } catch ( err ) { toast ( `확인: ${ err . message } ` ); if ( err . code === 'cli_not_logged_in' ) await openShell ( r . connection . cli && r . connection . cli . tool ? r . connection . cli . tool : 'claude' , 'login' ); } await loadConns (); }
else if ( next . action === 'test' ) { try { const t = await api ( 'POST' , `/ops/api/ai/connections/ ${ cid } /test` ); toast ( `확인됨 — 사용 가능 모델 ${ t . models . length } 개` ); } catch ( err ) { toast ( `확인 실패: ${ err . message } ` ); } await loadConns (); }
}
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
catch ( err ) { msg . textContent = err . message ; }
}
const CONN_ACT = {
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
'ai-conn-test' : async ( b ) => { try { const r = await api ( 'POST' , `/ops/api/ai/connections/ ${ b . id } /test` ); toast ( `확인됨 — 사용 가능 모델 ${ r . models . length } 개 ${ r . cliVersion ? ' · ' + r . cliVersion : '' } ` ); } catch ( e ) { toast ( `확인 실패: ${ e . message } ` ); if ( e . code === 'cli_not_logged_in' ) { const c = conns . find (( x ) => x . id === b . id ); await openShell ( c && c . cli && c . cli . tool ? c . cli . tool : 'claude' , 'login' ); } } await loadConns (); },
'ai-conn-oauth' : async ( b ) => { await startOauth ( b . id ); },
'ai-shell-open' : async ( b ) => { await openShell ( b . tool , b . command || 'login' ); },
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건 통과
2026-10-29 02:22:21 +00:00
'ai-shell-attach' : async ( b ) => { const sess = shellSessions . find (( x ) => x . id === b . id ) || { id : b . id , tool : b . tool , command : b . command , status : 'running' }; await openShell ( b . tool , b . command , sess ); },
'ai-shell-kill-id' : async ( b ) => { await api ( 'DELETE' , `/ops/api/ai/shell/ ${ b . id } ` ); toast ( '종료를 요청했습니다' ); setTimeout ( loadShells , 600 ); },
'ai-shell-kill-all' : async () => { await api ( 'POST' , '/ops/api/ai/shell/kill-all' ); toast ( '열린 셸을 모두 종료했습니다' ); setTimeout ( loadShells , 600 ); },
'ai-fix-login' : async ( b ) => { const c = ( conns . length ? conns : ( await api ( 'GET' , '/ops/api/ai/connections' )). connections ). find (( x ) => x . id === b . id ); if ( ! c ) return ; if ( c . authKind === 'oauth' ) return startOauth ( c . id ); if ( c . authKind === 'login' ) return openShell ( c . cli && c . cli . tool ? c . cli . tool : 'claude' , 'login' ); location . href = '/ops/ai' ; },
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
'ai-tools-group' : ( b ) => { for ( const x of f (). querySelectorAll ( `input[name=toolsEnabled][data-risk= ${ b . risk } ]` )) x . checked = b . on === '1' ; },
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
'ai-conn-secret' : async ( b ) => { const v = await ask ( 'API 키 교체' , [{ name : 'apiKey' , label : '새 API 키(저장 뒤 다시 표시하지 않음)' , type : 'password' , required : true }], { okLabel : '교체(재인증 필요)' }); if ( ! v ) return ; await call ( 'POST' , `/ops/api/ai/connections/ ${ b . id } /secret` , { apiKey : v . apiKey }); toast ( '키를 교체했습니다' ); await loadConns (); },
'ai-conn-edit' : ( b ) => fillForm ( conns . find (( c ) => c . id === b . id ) || null ),
2026-10-29 01:11:32 +00:00
'ai-cli-refresh' : () => loadClis (),
'ai-cli-use' : ( b ) => { fillForm ( null ); const form = f (); form . provider . value = 'local-cli' ; syncAuthFields (); form . authKind . value = 'login' ; syncAuthFields (); form . cliTool . value = b . tool ; form . model . value = b . model || '' ; form . name . value = form . name . value || ( b . tool === 'codex' ? '코덱스' : '클로드' ); form . scrollIntoView ({ behavior : 'smooth' }); form . name . focus (); },
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
'ai-conn-cancel' : () => { fillForm ( null ); f (). reset (); syncAuthFields (); },
'ai-conn-toggle' : async ( b ) => { await api ( 'PATCH' , `/ops/api/ai/connections/ ${ b . id } ` , { status : b . to }); await loadConns (); },
'ai-conn-delete' : async ( b ) => { const v = await ask ( '연결 삭제' , [{ name : 'name' , label : `삭제하려면 연결 이름 " ${ b . name } " 을 입력하세요` , required : true }], { okLabel : '삭제(재인증 필요)' , danger : true }); if ( ! v || v . name !== b . name ) return toast ( '이름이 다릅니다 — 삭제하지 않았습니다' ); await call ( 'DELETE' , `/ops/api/ai/connections/ ${ b . id } ` ); toast ( '삭제했습니다(Vault 비밀도 제거)' ); await loadConns (); await loadRooms (); },
};
// ---------------- 이벤트
document . addEventListener ( 'click' , async ( e ) => {
const tab = e . target . closest ( '.ai-tab[data-room]' ); if ( tab ) { e . preventDefault (); return switchRoom ( tab . dataset . room ). catch (( err ) => toast ( err . message )); }
const el = e . target . closest ( '[data-act]' ); if ( ! el ) return ; const act = el . dataset . act ;
if ( act === 'ai-toggle' ) { e . preventDefault (); const open = panel . classList . contains ( 'closed' ); saveState ({ open }); applyState (); if ( open ) loadMessages ({ full : true }). catch (() => {}); return ; }
if ( act === 'ai-reset' ) { e . preventDefault (); if ( ! current ) return ; const v = await ask ( '새 대화' , [{ name : 'ok' , label : '이 방의 대화 기록을 비우고 승인 대기 제안을 취소합니다' , type : 'checkbox' }], { okLabel : '비우기' , danger : true }); if ( ! v || v . ok !== 'on' ) return ; await api ( 'POST' , `/ops/api/ai/rooms/ ${ encodeURIComponent ( current ) } /reset` ). catch (( err ) => toast ( err . message )); return loadMessages ({ full : true }); }
if ( act === 'ai-approve' ) { e . preventDefault (); return approve ( el . dataset . id , el . dataset . risk , ( el . dataset . ids || '' ). split ( ',' ). filter ( Boolean ), el . dataset . tool ). catch (( err ) => toast ( err . message )); }
if ( act === 'ai-reject' ) { e . preventDefault (); return reject ( el . dataset . id ). catch (( err ) => toast ( err . message )); }
if ( CONN_ACT [ act ]) { e . preventDefault (); try { await CONN_ACT [ act ]( el . dataset ); } catch ( err ) { toast ( err . message ); } }
});
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건 통과
2026-10-29 02:22:21 +00:00
document . addEventListener ( 'change' , async ( e ) => { const sel = e . target . closest ( 'select[data-act=ai-model-pick]' ); if ( ! sel ) return ; try { await api ( 'PATCH' , `/ops/api/ai/connections/ ${ sel . dataset . id } ` , { model : sel . value }); toast ( `모델을 ${ sel . value } 로 바꿨습니다` ); if ( PAGE_AI ) loadConns (); else loadRooms (); } catch ( err ) { toast ( err . message ); } }); // [0.3.9] 모델 선택
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
document . addEventListener ( 'dblclick' , async ( e ) => { const tab = e . target . closest ( '.ai-tab[data-room]:not(.fixed)' ); if ( ! tab ) return ; const r = rooms . find (( x ) => x . id === tab . dataset . room ); const v = await ask ( '탭 이름 바꾸기' , [{ name : 'title' , label : '탭 이름(모델 이름이 기본값)' , value : r ? r . title : '' , required : true }]); if ( ! v ) return ; try { await api ( 'PATCH' , `/ops/api/ai/rooms/ ${ encodeURIComponent ( tab . dataset . room ) } ` , { title : v . title }); await loadRooms (); } catch ( err ) { toast ( err . message ); } });
if ( panel ) {
$ ( '#ai-compose' ). addEventListener ( 'submit' , ( e ) => { e . preventDefault (); send (); });
$ ( '#ai-input' ). addEventListener ( 'keydown' , ( e ) => { if (( e . ctrlKey || e . metaKey ) && e . key === 'Enter' ) { e . preventDefault (); send (); } });
const rs = $ ( '.ai-resize' , panel ); let drag = null ;
rs . addEventListener ( 'pointerdown' , ( e ) => { drag = { x : e . clientX , w : panel . getBoundingClientRect (). width }; rs . setPointerCapture ( e . pointerId ); });
rs . addEventListener ( 'pointermove' , ( e ) => { if ( ! drag ) return ; const w = Math . max ( 280 , Math . min ( window . innerWidth * 0.6 , drag . w + ( drag . x - e . clientX ))); panel . style . setProperty ( '--ai-w' , w + 'px' ); });
rs . addEventListener ( 'pointerup' , () => { if ( drag ) saveState ({ width : panel . getBoundingClientRect (). width }); drag = null ; });
window . addEventListener ( 'resize' , applyState ); document . addEventListener ( 'visibilitychange' , () => { if ( ! document . hidden ) schedule ( 300 ); });
applyState ();
loadRooms (). then (() => loadMessages ({ full : true })). catch (( e ) => { $ ( '#ai-tabs' ). innerHTML = `<span class="err small">AI 채팅룸을 불러오지 못했습니다: ${ A . esc ( e . message ) } </span>` ; });
}
if ( PAGE_AI ) {
HAL 0.3.2: AI 연동 편의성 + 도구 기본 설정·승인 요청 — 공급자 프리셋(Claude·ChatGPT·Gemini·OpenRouter + 기타 OpenAI 호환, Gemini 자체 API 어댑터), 사전 준비 안내, OAuth 저장 직후 로그인 창 자동(완료 페이지 postMessage·닫기, Gemini Google 프리셋, OpenRouter PKCE 키 발급), CLI 로그인용 텍스트 셸(허용 명령만 PTY, 재인증, 입력 미저장, 미로그인 시 자동 열기), 연결별 도구 정책(기본 전부)·로컬 CLI 에 HAL MCP 설정 자동 주입·MCP 설정 스니펫, MCP 쓰기·고위험 호출은 승인 요청(제안)으로 + hal_action_status/hal_actions_pending, 개발 지시 v0.3.2(Oz)·docs/HAL-AI.md §7·VERSION_HISTORY 0.3.2 — test/hal-ai-032.test.mjs RED 0/6 → GREEN 6/6, 전체 27파일 189건 통과
2026-10-29 01:43:02 +00:00
const form = f (); form . provider . addEventListener ( 'change' , () => { syncAuthFields (); }); form . authKind . addEventListener ( 'change' , syncAuthFields ); form . toolsPreset . addEventListener ( 'change' , renderToolPolicy ); form . addEventListener ( 'submit' , submitConn ); syncAuthFields ();
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건 통과
2026-10-29 02:22:21 +00:00
loadConns (). catch (( e ) => { $ ( '#ai-conns' ). innerHTML = `<div class="aw-state failed"> ${ A . esc ( e . message ) } </div>` ; }); loadClis (); loadTools (); loadMcp (); loadShells ();
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
const q = new URLSearchParams ( location . search ); if ( q . get ( 'oauth' ) === 'ok' ) toast ( 'OAuth 로그인이 완료되어 토큰을 Vault 에 저장했습니다 — "확인"으로 모델 목록을 받아 두세요' );
}