Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 71 additions & 6 deletions docs/ipad-input-session-diagnostics-20260928.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,27 @@ vault: 미설정

문서 원본은 이 파일이다. 지정 Obsidian Vault 연결은 미설정이다.
요구사항·진행 상태: [이슈 #20](https://github.com/Jaeman-Lee/dotfiles/issues/20).
최종 기록·입력 개선 검토: [draft PR #23](https://github.com/Jaeman-Lee/dotfiles/pull/23).
초기 진단 기록: [커밋 fb25194](https://github.com/Jaeman-Lee/dotfiles/commit/fb25194).
기존 입력 운영 절차는 [Ubuntu 웹 원격 화면](ubuntu-web-desktop.md#ipad)을 따른다.
실행 로그·대화 원문·세션 식별자·인증 정보는 기록하지 않는다.

## 확인 결과
## 이번 작업의 마감

2026-09-28 사용자 요청에 따라 이번 원격 테스트와 진단은 여기까지 진행하고 Git 기록을 남긴다.
운영 배포·서비스 재시작·PR 병합은 수행하지 않는다. 해결되지 않은 항목은 이슈에서 후속 보류로 유지한다.

- iPad의 기존 **글쓰기 → 전송** 경로에서 긴 한글 문장이 정상 수신되는 것을 사용자와 확인했다.
- 사용자는 글쓰기 클릭 후 간헐적으로 PC 터미널로 입력되는 현상도 보고했다.
포커스 보호·접기 수정과 Ctrl+Enter 전송은 구현 및 검증을 마쳤지만 **운영에는 미적용**이다.
구현·검증 원본은 [운영 문서의 변경 기록](ubuntu-web-desktop.md#글쓰기-포커스-보호와-ctrlenter--2026-09-28)을 따른다.
- 직접 입력의 편의성을 우선하고 글쓰기는 보조 수단으로 남기는 방향을 사용자가 선호했다.
직접 입력 개선은 구현하지 않았다. 실제 브라우저와 외장/화면 키보드 구분도 확인이 남았다.
- 모델 응답 연결 종료 및 재시도 기록과 두 작업의 후속 완료를 확인했다.
다만 이후 사용자가 제공한 캡처는 별도의 **app-server 연결 상실 및 세션 자동 복원 실패**였다.
아래 두 작업의 완료를 캡처 속 세션 복구의 증거로 사용할 수 없다.

## 초기 확인 결과

14:47–14:51 KST에 읽기 전용으로 확인했다. 해당 시점의 관측이며 간헐적인 장애를 배제하지 않는다.

Expand All @@ -31,16 +48,64 @@ vault: 미설정
기존 운영 문서에는 Guacamole Text input 경로에서 한글 누락과 PC 입력기의 영문 재해석을
실제 호스트에서 재현한 기록이 있다. iPad 사용자는 하단 **글쓰기**의 흰 입력칸에서
문장을 완성하고 **전송 ↵**을 누르는 기존 기능을 우선 사용한다.
실제 사용 중인 브라우저·원격 앱, 외장/화면 키보드 여부와 이 입력칸에서도 문제가 발생하는지는 확인이 남았다.
후속 확인에서 이 입력칸을 통한 한글 문장 전송은 성공했다. 간헐적인 포커스 문제는 별도로 보고되었다.

## 검증과 남은 확인
## 초기 진단의 검증 범위

`node --test remote-desktop/tests/mobile-input.test.cjs`: 기존 회귀 테스트 9개 통과.
이는 전송 순서·중복 방지·초안 보존 검증이며 iPad 실기기 한글 조합 성공을 증명하지 않는다.

소스 코드, PC 입력기 설정, 원격 서비스와 Codex 실행 상태는 변경하지 않았다.
초기 진단에서는 소스 코드, PC 입력기 설정, 원격 서비스와 Codex 실행 상태를 변경하지 않았다.
기존 세션이나 daemon을 재시작하지 않았다. 미전송 입력과 실행 중 작업을 보존했다.
실제 오류 문구와 영향받는 세션의 연결 상태를 확인한 뒤 복구 범위를 결정한다.
한글 입력 문제와 세션 연결 문제 모두 해결 완료로 처리하지 않는다.
후속 소스 수정과 17개 회귀 테스트 결과는 운영 문서에 기록했으며, 운영 서비스는 계속 유지했다.

## Codex 재접속 후속 확인

2026-09-28 22:12–22:17 KST에 보존된 로그와 작업 완료 이벤트를 읽기 전용으로 확인했다.
다음 시각은 모두 KST이며, 확인 가능한 사례만 기술한다.

| 시각 | 관측 | 확인 범위 |
| --- | --- | --- |
| 09-27 19:57:36, 20:58:13 | 응답 WebSocket의 60분 연결 제한에 도달했다는 오류 후 재시도 | 이 두 건의 재접속 사유가 오류에 명시되어 있다. |
| 09-28 18:30:18 | 응답 완료 전 `Connection closed normally`로 연결 종료, 재시도 1/5 | 같은 대화의 작업 완료 이벤트가 18:30:52에 기록되어 약 34초 뒤 완료를 확인했다. |
| 09-28 19:38:04 | 같은 연결 종료와 재시도 1/5 | 작업 완료 이벤트가 19:38:18에 기록되어 약 14초 뒤 완료를 확인했다. |
| 09-28 22:11:48, 22:11:57 | 로컬 제어 소켓의 WebSocket handshake 미완료 | 앞선 모델 서버 응답 연결 종료와 동일한 원인이라고 단정하지 않는다. |

`Connection closed normally`는 연결 종료 방식의 설명이며, 답변 생성이 성공적으로 끝났다는 의미가 아니다.
오늘 두 건에서 연결을 닫은 근본 원인이 서버인지 네트워크인지 로그만으로 확정할 수 없다.
별도로 여러 터미널에서 `account/rateLimits/read` 시간 초과가 반복되었다.
이는 조회 응답이 지연된 기록이며, 사용량 한도를 소진했다는 증거는 아니다.

원격 웹 컨테이너는 확인 당시 약 23시간 연속 실행 중이었고 OOM 종료 흔적은 없었다.
GNOME 원격 서비스도 유지되어 있었다. 이번 작업에서 원격 웹·Codex daemon을 재시작하거나
수정본을 운영에 적용한 사실은 없다. 이 관측만으로 과거의 모든 네트워크 끊김을 배제하지 않는다.

### 사용자 캡처 확인에 따른 정정

사용자가 안내한 로컬 Screenshots 폴더의 2026-09-28 22:23:46 캡처를 직접 확인했다.
캡처 원본과 화면의 개인 작업 내용은 Git에 포함하지 않고 진단에 필요한 오류 문구만 기록한다.

- `Connection lost. Attempting to reconnect…`
- `Automatic reconnect could not restore this session.`
- `app-server session could not be restored`
- 화면에는 Ctrl+C 후 같은 명령으로 재연결하라는 안내도 표시되어 있다.

이 오류는 Codex 터미널과 app-server 사이의 연결을 잃은 뒤 기존 세션을 자동 복원하지
못한 상태를 보여 준다. 앞서 모델 응답 스트림의 재시도만으로 사용자 증상을 설명한 것은
불충분했다. 단순 모델 응답 재시도와 터미널 세션 자동 복원 실패를 구분하여 후속 조사해야 한다.
캡처만으로 대화 기록 삭제나 작업 유실 여부를 판단할 수 없다.

현재 daemon 프로세스 시작 시각은 09-28 14:25:18 KST이며 CLI 0.157.0 / daemon 0.158.0
조합을 다시 확인했다. 22:20:34와 22:24:05에는 app-server WebSocket의 closing handshake
없는 connection reset도 기록되었다. 캡처 시각이 실제 최초 장애 시각과 같다고 볼 수 없으므로
이 시각·버전 차이·주변 연결 오류를 곧바로 해당 실패의 원인으로 단정하지 않는다.
재시작·업데이트·세션 재연결은 수행하지 않았으며 이번 추가 확인은 진단 기록 정정으로 한정한다.

## 후속 작업의 경계

실제 할 일과 상태는 [이슈 #20](https://github.com/Jaeman-Lee/dotfiles/issues/20) 및
[입력 개선 이슈 #22](https://github.com/Jaeman-Lee/dotfiles/issues/22)에서 관리한다.
다시 진행할 때는 운영 적용 여부를 먼저 정하고, 적용한다면 기기 새로고침 후 iPad 실기기 포커스·단축키를 확인한다.
직접 한글 입력 개선과 반복 재접속의 근본 원인 조사는 별도 후속 범위다.

참고: [OpenAI 공식 설정 문서](https://developers.openai.com/codex/config-reference/).
38 changes: 38 additions & 0 deletions docs/ubuntu-web-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,44 @@ PC 클립보드는 작성한 문장으로 바뀐다. 문장은 연결 화면을
글쓰기 방식은 기기에서 조합을 끝낸 문장을 클립보드로 보내 이 문제를 피한다.
PC의 전역 한/영 단축키는 임의로 변경하지 않는다.

### 글쓰기 포커스 보호와 Ctrl+Enter — 2026-09-28

요구사항·진행 상태: [이슈 #22](https://github.com/Jaeman-Lee/dotfiles/issues/22).
변경 검토: [draft PR #23](https://github.com/Jaeman-Lee/dotfiles/pull/23),
구현·검증: [커밋 756a53e](https://github.com/Jaeman-Lee/dotfiles/commit/756a53e).
**소스 수정·검증 완료, 운영 적용 대기.** 현재 원격 접속을 끊는 웹 컨테이너 재시작은 아직 수행하지 않았다.
사용자 요청으로 이번 테스트는 여기서 마감하고 운영 적용과 실기기 후속 확인은 보류한다.
최종 수신 확인·재접속 진단과 작업 범위는 [원격 테스트 마감 기록](ipad-input-session-diagnostics-20260928.md)에 남긴다.
아래 동작은 변경본을 적용하고 기기 페이지를 새로고침한 뒤 사용할 수 있다.

- **글쓰기**를 누르면 표시를 동기적으로 갱신한 뒤 흰 입력칸에 초점을 둔다.
- 글쓰기 모드에서는 입력칸이 잠깐 초점을 잃어도 물리 키 이벤트를 PC로 전달하지 않는다.
Guacamole의 숨은 입력칸이 초점을 가져가려 하면 흰 입력칸으로 되돌린다.
PC 화면 클릭과 대상 선택은 가능하다. 직접 PC에 타이핑하려면 **접기**를 누른다.
- 흰 입력칸의 **Ctrl+Enter**는 **전송 ↵**과 같다. **Enter**는 줄바꿈이다.
조합 중인 한글, 빈 문장, 키 자동 반복으로는 전송하지 않는다. 전송 중 연타는 기존 전송 잠금을 따른다.
- 버튼 전송과 붙여넣기 후에도 입력칸에 초점을 두어 다음 문장을 이어서 작성할 수 있다.

원인과 수정: 이전 코드는 textarea에 실제 초점이 있을 때만 키 전달을 막았다.
입력칸 표시에는 `ng-show`가 사용되어 표시 전환과 동기적인 `focus()`를 분리하지 않았다.
변경본은 애니메이션 없는 `ng-style` 표시와 모드 전체의 입력 차단을 사용한다.
숨은 InputSink의 focus 핸들러는 비동기 select를 예약하므로, focus 캡처 단계에서 차단해
반복 재포커스가 발생하지 않도록 했다. 또한 `ng-if` 내부의 접기 버튼이 자식 scope에
`writing`을 따로 생성하여 실제 글쓰기 모드를 끄지 못하는 문제를 브라우저에서 재현했고,
directive scope의 함수로 상태를 바꾸도록 수정했다. iPad에서 보고된 간헐 현상의 정확한
이벤트 순서는 아직 실기기에서 계측하지 않았으므로 모든 원인을 확정한 것으로 보지 않는다.

검증: Node 회귀 테스트 17개 통과. 실제 Guacamole 1.6.0 Keyboard/InputSink와 Angular를
사용한 별도 브라우저 fixture에서 Ctrl+Enter의 클립보드→붙여넣기→Enter 순서,
전송 후 비우기, Enter 줄바꿈, 조합 중 전송 차단, 초점 이탈 시 원격 키 차단,
숨은 입력칸의 포커스 회수와 접기 후 직접 키 입력 재개를 확인했다.
이 fixture는 실제 PC 터미널에 연결하지 않는다. 1024px 브라우저 표시도 확인했으나
iPad Safari 실기기 및 변경본의 RDP 종단간 전송은 별도 확인이 남아 있다.

사용자가 이번 수정 전 기존 **글쓰기→전송** 경로에서 긴 한글 문장의 정상 수신을 확인했다.
이는 [앞선 진단](ipad-input-session-diagnostics-20260928.md)의 기본 한글 전송 확인을 갱신한다.
기존 Codex 세 세션의 연결 재시도는 별도 문제로, 이 입력 UI 변경의 해결 범위에 포함하지 않는다.

갤럭시와 iPad 모두 주소를 즐겨찾기에 저장하면 다음 접속이 간단하다.
두 기기에서 동시에 연결하는 기능은 별도 검증하지 않았으므로 우선 한 기기씩 사용한다.

Expand Down
51 changes: 49 additions & 2 deletions remote-desktop/mobile-input/mobile-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,26 @@ angular.module('client').directive('ubuntuMobileInput', ['$window', '$timeout',
if (scope.writing) scope.menu.inputMethod = 'none';

var draft = element[0].querySelector('#ubuntu-writing-text');
var composing = false;
function compositionStarted() { composing = true; }
function compositionEnded() { composing = false; }
draft.addEventListener('compositionstart', compositionStarted);
draft.addEventListener('compositionend', compositionEnded);

// The native keyboard/IME edits this local draft. Never forward its
// physical key events to the remote client as well.
function localKey(event) { event.stopPropagation(); }
function localKey(event) {
event.stopPropagation();
if (event.type === 'keydown' && event.key === 'Enter' && event.ctrlKey &&
!event.altKey && !event.metaKey && !event.shiftKey &&
!composing && !event.isComposing && event.keyCode !== 229) {
event.preventDefault();
// An empty/repeated shortcut must never execute a bare PC Enter.
if (!event.repeat && scope.draft) {
scope.$apply(function () { scope.pasteDraft(true); });
}
}
}
['keydown', 'keyup', 'keypress'].forEach(function (name) {
draft.addEventListener(name, localKey);
});
Expand All @@ -24,10 +41,28 @@ angular.module('client').directive('ubuntuMobileInput', ['$window', '$timeout',
// local form editing out of the remote desktop.
['guacBeforeKeydown', 'guacBeforeKeyup'].forEach(function (name) {
scope.$on(name, function (event) {
if ($window.document.activeElement === draft) event.preventDefault();
// Focus can briefly land on Guacamole's hidden input sink,
// especially after a touch or browser focus transition.
// Fail closed for the entire writing mode, not just textarea focus.
if (scope.writing || $window.document.activeElement === draft)
event.preventDefault();
});
});

function protectFocus(event) {
if (!scope.writing || scope.menu.shown || event.target === draft) return;
var target = event.target;
if (target.tagName !== 'TEXTAREA') return;
var rect = target.getBoundingClientRect();
if (target.id === 'ubuntu-remote-text' || (!rect.width && !rect.height)) {
// Stop before InputSink's target focus listener schedules a
// select()/refocus timer, or the two fields can fight forever.
event.stopImmediatePropagation();
draft.focus();
}
}
$window.document.addEventListener('focus', protectFocus, true);

var writingButton = element[0].querySelector('.ubuntu-open-writing');
function openWriting() {
scope.$apply(function () {
Expand All @@ -38,6 +73,12 @@ angular.module('client').directive('ubuntuMobileInput', ['$window', '$timeout',
draft.focus();
}
writingButton.addEventListener('click', openWriting);
scope.closeWriting = function () {
// The button lives inside ng-if's child scope. Change the
// directive scope here instead of shadowing `writing` there.
scope.writing = false;
scope.menu.inputMethod = 'none';
};

var pendingPaste, pendingEnter;
var lastPaste = null;
Expand All @@ -55,6 +96,9 @@ angular.module('client').directive('ubuntuMobileInput', ['$window', '$timeout',
return;
}
scope.sending = true;
// Keep typing locally after clicking Send/Paste, including on iPad
// where opening the keyboard needs the original user gesture.
if (scope.writing) draft.focus();
var client = managed.client;
var terminal = scope.terminal;
var text = scope.draft;
Expand Down Expand Up @@ -153,6 +197,9 @@ angular.module('client').directive('ubuntuMobileInput', ['$window', '$timeout',
scope.$on('$destroy', function () {
button.removeEventListener('click', openKeyboard);
writingButton.removeEventListener('click', openWriting);
$window.document.removeEventListener('focus', protectFocus, true);
draft.removeEventListener('compositionstart', compositionStarted);
draft.removeEventListener('compositionend', compositionEnded);
['keydown', 'keyup', 'keypress'].forEach(function (name) {
draft.removeEventListener(name, localKey);
});
Expand Down
10 changes: 5 additions & 5 deletions remote-desktop/mobile-input/toolbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@
<div class="ubuntu-input-actions" role="group" aria-label="원격 입력">
<button type="button" class="ubuntu-open-writing">글쓰기</button>
<button type="button" class="ubuntu-open-keyboard">키보드</button>
<button type="button" ng-if="writing || menu.inputMethod === 'text'" ng-click="writing = false; menu.inputMethod = 'none'">접기</button>
<button type="button" ng-if="writing || menu.inputMethod === 'text'" ng-click="closeWriting()">접기</button>
<button type="button" ng-click="sendEnter()" ng-disabled="sending" title="글쓰기의 문장을 PC에 붙여넣고 Enter">Enter</button>
<guac-key keysym="65288" text="'⌫'"></guac-key>
<button type="button" ng-click="menu.shown = !menu.shown">메뉴</button>
</div>
<div class="ubuntu-writing" ng-show="writing">
<div class="ubuntu-writing" ng-style="{display: writing ? 'block' : 'none'}">
<label class="ubuntu-input-destination">입력 대상
<select ng-model="terminal" ng-disabled="sending" ng-change="draftEdited()" aria-label="입력 대상">
<option ng-value="true">Codex · 터미널</option>
<option ng-value="false">브라우저 · 일반 앱</option>
</select>
</label>
<label for="ubuntu-writing-text">PC에서 입력할 곳을 누른 뒤, 여기에 글을 쓰세요.</label>
<textarea id="ubuntu-writing-text" rows="2" aria-label="PC에 보낼 문장" ng-model="draft" ng-change="draftEdited()" autocapitalize="off" spellcheck="false"></textarea>
<label for="ubuntu-writing-text">여기에 글쓰기 · Ctrl+Enter로 전송 · Enter로 줄바꿈</label>
<textarea id="ubuntu-writing-text" rows="2" aria-label="PC에 보낼 문장" aria-keyshortcuts="Control+Enter" ng-model="draft" ng-change="draftEdited()" autocapitalize="off" spellcheck="false"></textarea>
<div class="ubuntu-writing-actions">
<button type="button" ng-click="pasteDraft(true)" ng-disabled="!draft || sending">전송 ↵</button>
<button type="button" ng-click="pasteDraft(false)" ng-disabled="!draft || sending">붙여넣기만</button>
<button type="button" ng-click="draft = ''; draftEdited()" ng-disabled="sending">지우기</button>
</div>
<div class="ubuntu-input-notice" role="status">{{notice || '한/영은 기기 키보드에서 전환 · PC 클립보드를 사용합니다.'}}</div>
<div class="ubuntu-input-notice" role="status">{{notice || '글쓰기 중에는 PC 직접 타이핑이 잠깁니다. 직접 입력하려면 접기를 누르세요.'}}</div>
</div>
<label for="ubuntu-remote-text" ng-show="menu.inputMethod === 'text'">아래 입력칸을 눌러 입력 · 한/영은 기기 키보드에서 전환</label>
</ubuntu-mobile-input>
Loading
Loading