mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-31 13:19:33 +01:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4aac8029 | ||
|
|
d436c6375f | ||
|
|
e96baf9f66 |
No files matched your search
@@ -1,5 +1,21 @@
|
|||||||
# aicodeman
|
# aicodeman
|
||||||
|
|
||||||
|
## 0.4.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Fix mobile keyboard toolbar positioning on iOS Safari: toolbar (Run/Stop/Run Shell) was hidden behind the accessory bar when virtual keyboard was active due to overlapping CSS positions. Remove the aggressive safety check in `updateLayoutForKeyboard()` that incorrectly dismissed keyboard state when iOS scrolled the visual viewport during typing. Add Safari-bar CSS offset to accessory bar so it properly stacks above the toolbar. Remove the double-counted Safari-bar offset when keyboard is visible since the JS transform already covers the full distance.
|
||||||
|
|
||||||
|
## 0.4.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- fix: mobile keyboard hides terminal content on iPhone
|
||||||
|
|
||||||
|
Fixed a bug where opening the virtual keyboard on iPhone left zero visible terminal space. Two independent mechanisms were both accounting for the keyboard height: `MobileDetection.updateAppHeight()` shrunk `--app-height` to the visual viewport height, while `KeyboardHandler.updateLayoutForKeyboard()` added a large `paddingBottom`. These double-counted, leaving negative space for the terminal (user saw accessory bar + toolbar but no terminal content).
|
||||||
|
|
||||||
|
Fix: `updateAppHeight()` now skips when the keyboard is visible, and `handleViewportResize()` restores `--app-height` to the pre-keyboard value on first detection (since MobileDetection's listener fires before KeyboardHandler's). On keyboard close, `--app-height` is re-synced to the current visual viewport.
|
||||||
|
|
||||||
## 0.4.3
|
## 0.4.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ When user says "COM":
|
|||||||
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
|
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
|
||||||
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
|
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
|
||||||
|
|
||||||
**Version**: 0.4.3 (must match `package.json`)
|
**Version**: 0.4.5 (must match `package.json`)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aicodeman",
|
"name": "aicodeman",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
|
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -100,8 +100,12 @@ const MobileDetection = {
|
|||||||
|
|
||||||
/** Set --app-height CSS variable from visual viewport.
|
/** Set --app-height CSS variable from visual viewport.
|
||||||
* On iPad Safari with tabs, 100vh extends behind the tab bar.
|
* On iPad Safari with tabs, 100vh extends behind the tab bar.
|
||||||
* visualViewport.height reflects the actual visible area. */
|
* visualViewport.height reflects the actual visible area.
|
||||||
|
* Skips when virtual keyboard is open — KeyboardHandler manages
|
||||||
|
* layout via translateY + paddingBottom; shrinking --app-height
|
||||||
|
* would double-count and leave zero space for the terminal. */
|
||||||
updateAppHeight() {
|
updateAppHeight() {
|
||||||
|
if (typeof KeyboardHandler !== 'undefined' && KeyboardHandler.keyboardVisible) return;
|
||||||
const vh = window.visualViewport?.height || window.innerHeight;
|
const vh = window.visualViewport?.height || window.innerHeight;
|
||||||
document.documentElement.style.setProperty('--app-height', `${vh}px`);
|
document.documentElement.style.setProperty('--app-height', `${vh}px`);
|
||||||
},
|
},
|
||||||
@@ -225,6 +229,10 @@ const KeyboardHandler = {
|
|||||||
if (heightDiff > 150 && !this.keyboardVisible) {
|
if (heightDiff > 150 && !this.keyboardVisible) {
|
||||||
this.keyboardVisible = true;
|
this.keyboardVisible = true;
|
||||||
document.body.classList.add('keyboard-visible');
|
document.body.classList.add('keyboard-visible');
|
||||||
|
// Restore --app-height: MobileDetection's resize listener fires before ours
|
||||||
|
// and may have already shrunk it for the keyboard viewport change.
|
||||||
|
// Use initialViewportHeight (captured before keyboard opened).
|
||||||
|
document.documentElement.style.setProperty('--app-height', `${this.initialViewportHeight}px`);
|
||||||
this.onKeyboardShow();
|
this.onKeyboardShow();
|
||||||
}
|
}
|
||||||
// Keyboard hidden (viewport grew back close to initial)
|
// Keyboard hidden (viewport grew back close to initial)
|
||||||
@@ -234,6 +242,9 @@ const KeyboardHandler = {
|
|||||||
this.keyboardVisible = false;
|
this.keyboardVisible = false;
|
||||||
document.body.classList.remove('keyboard-visible');
|
document.body.classList.remove('keyboard-visible');
|
||||||
this.onKeyboardHide();
|
this.onKeyboardHide();
|
||||||
|
// Re-sync --app-height now that keyboard is gone (MobileDetection skipped
|
||||||
|
// updates while keyboardVisible was true)
|
||||||
|
MobileDetection.updateAppHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update baseline when keyboard is not visible — adapts to address bar
|
// Update baseline when keyboard is not visible — adapts to address bar
|
||||||
@@ -261,35 +272,31 @@ const KeyboardHandler = {
|
|||||||
const main = document.querySelector('.main');
|
const main = document.querySelector('.main');
|
||||||
|
|
||||||
if (this.keyboardVisible) {
|
if (this.keyboardVisible) {
|
||||||
// Calculate keyboard offset
|
// Calculate how far the toolbar (position:fixed, bottom:0) needs to
|
||||||
|
// translate up so it sits at the bottom of the visual viewport.
|
||||||
|
// This formula accounts for iOS scrolling the visual viewport (offsetTop)
|
||||||
|
// when the user types in xterm's hidden textarea.
|
||||||
const layoutHeight = window.innerHeight;
|
const layoutHeight = window.innerHeight;
|
||||||
const visualBottom = window.visualViewport.offsetTop + window.visualViewport.height;
|
const visualBottom = window.visualViewport.offsetTop + window.visualViewport.height;
|
||||||
const keyboardOffset = layoutHeight - visualBottom;
|
const keyboardOffset = Math.max(0, layoutHeight - visualBottom);
|
||||||
|
|
||||||
// Safety: if keyboard is supposedly visible but offset is 0 or negative,
|
// Move toolbar and accessory bar above keyboard.
|
||||||
// the keyboard is actually gone — force dismiss. This catches cases where
|
// When keyboardOffset is 0 (viewport scrolled to layout bottom),
|
||||||
// visualViewport.resize fires late or with intermediate values on iOS.
|
// the bars are naturally positioned via their CSS bottom values —
|
||||||
if (keyboardOffset <= 0) {
|
// just clear the transforms. Never dismiss keyboard state here;
|
||||||
this.keyboardVisible = false;
|
// that's handleViewportResize's job.
|
||||||
document.body.classList.remove('keyboard-visible');
|
|
||||||
this.onKeyboardHide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move toolbar up above keyboard
|
|
||||||
if (toolbar) {
|
if (toolbar) {
|
||||||
toolbar.style.transform = `translateY(${-keyboardOffset}px)`;
|
toolbar.style.transform = keyboardOffset > 0 ? `translateY(${-keyboardOffset}px)` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move accessory bar up (it sits above toolbar)
|
|
||||||
if (accessoryBar) {
|
if (accessoryBar) {
|
||||||
accessoryBar.style.transform = `translateY(${-keyboardOffset}px)`;
|
accessoryBar.style.transform = keyboardOffset > 0 ? `translateY(${-keyboardOffset}px)` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shrink main content area so terminal doesn't extend behind keyboard
|
// Shrink main content area so terminal doesn't extend behind keyboard.
|
||||||
// Account for keyboard height + toolbar height (40px) + accessory bar (44px)
|
// Use stable keyboard height (not scroll-dependent) for padding.
|
||||||
if (main) {
|
const keyboardHeight = this.initialViewportHeight - (window.visualViewport.height || window.innerHeight);
|
||||||
main.style.paddingBottom = `${keyboardOffset + 94}px`;
|
if (main && keyboardHeight > 0) {
|
||||||
|
main.style.paddingBottom = `${keyboardHeight + 94}px`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.resetLayout();
|
this.resetLayout();
|
||||||
|
|||||||
@@ -534,6 +534,17 @@ html.mobile-init .file-browser-panel {
|
|||||||
bottom: calc(var(--safe-area-bottom) + (100vh - var(--app-height, 100vh)));
|
bottom: calc(var(--safe-area-bottom) + (100vh - var(--app-height, 100vh)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* When keyboard is visible the JS translateY already accounts for the full
|
||||||
|
distance from the visual-viewport bottom to the layout-viewport bottom
|
||||||
|
(keyboard + Safari bar). Remove the CSS Safari-bar offset to avoid
|
||||||
|
double-counting, which otherwise creates a visible gap above the keyboard. */
|
||||||
|
.keyboard-visible.ios-device.safari-browser .toolbar {
|
||||||
|
bottom: var(--safe-area-bottom);
|
||||||
|
}
|
||||||
|
.keyboard-visible.ios-device.safari-browser .keyboard-accessory-bar {
|
||||||
|
bottom: calc(var(--safe-area-bottom) + 40px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Show case selector in center */
|
/* Show case selector in center */
|
||||||
.toolbar-center {
|
.toolbar-center {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
|
|||||||
@@ -51,6 +51,39 @@ const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
|
|||||||
const CTRL_L_PATTERN = /\x0c/g;
|
const CTRL_L_PATTERN = /\x0c/g;
|
||||||
const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;
|
const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip redundant Ink spinner/status-bar redraw frames from the terminal buffer.
|
||||||
|
* Ink (Claude Code's TUI) uses absolute cursor positioning (CSI n d = VPA, CSI n;m H = CUP)
|
||||||
|
* to animate the spinner and update the status bar. During long thinking phases, these frames
|
||||||
|
* accumulate to 500KB+ of repeated overwrites to the same rows. When the buffer is tailed,
|
||||||
|
* only spinner frames are returned, making the terminal appear empty.
|
||||||
|
*
|
||||||
|
* Strategy: find where absolute-positioned redraws begin (first VPA sequence), then keep
|
||||||
|
* only the last ~4KB of redraw frames (the final visual state) and discard the rest.
|
||||||
|
*/
|
||||||
|
function stripInkRedrawBloat(buffer: string): string {
|
||||||
|
// Find where Ink's absolute-positioned redraws start (first CSI n d = VPA)
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const firstVPA = buffer.search(/\x1b\[\d+d/);
|
||||||
|
if (firstVPA === -1) return buffer; // No Ink redraws
|
||||||
|
|
||||||
|
const contentPart = buffer.slice(0, firstVPA);
|
||||||
|
const redrawPart = buffer.slice(firstVPA);
|
||||||
|
|
||||||
|
// If the redraw section is small (<16KB), not worth stripping
|
||||||
|
if (redrawPart.length < 16384) return buffer;
|
||||||
|
|
||||||
|
// Keep only the last 4KB of redraw frames — this preserves the final visual state
|
||||||
|
// (spinner position, status bar text, token count, etc.)
|
||||||
|
const tail = redrawPart.slice(-4096);
|
||||||
|
// Avoid starting mid-escape: find first complete frame boundary
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const frameStart = tail.search(/\x1b\(B\x1b\[m|\x1b\[\d+d|\x1b\[\d+;\d+H/);
|
||||||
|
const cleanTail = frameStart > 0 ? tail.slice(frameStart) : tail;
|
||||||
|
|
||||||
|
return contentPart + cleanTail;
|
||||||
|
}
|
||||||
|
|
||||||
export function registerSessionRoutes(
|
export function registerSessionRoutes(
|
||||||
app: FastifyInstance,
|
app: FastifyInstance,
|
||||||
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort
|
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort
|
||||||
@@ -534,10 +567,16 @@ export function registerSessionRoutes(
|
|||||||
let truncated = false;
|
let truncated = false;
|
||||||
let cleanBuffer: string;
|
let cleanBuffer: string;
|
||||||
|
|
||||||
if (tailBytes > 0 && fullSize > tailBytes) {
|
// Strip redundant Ink spinner/status redraws BEFORE tailing.
|
||||||
|
// During long thinking phases, Ink rewrites the same rows thousands of times
|
||||||
|
// (500KB+). Without stripping, tail mode returns only spinner frames and
|
||||||
|
// the terminal appears empty when switching tabs.
|
||||||
|
const strippedBuffer = stripInkRedrawBloat(session.terminalBuffer);
|
||||||
|
|
||||||
|
if (tailBytes > 0 && strippedBuffer.length > tailBytes) {
|
||||||
// Fast path: tail from the end, skip expensive banner search on full 2MB buffer.
|
// Fast path: tail from the end, skip expensive banner search on full 2MB buffer.
|
||||||
// Banner is near the top and gets discarded by tail anyway.
|
// Banner is near the top and gets discarded by tail anyway.
|
||||||
cleanBuffer = session.terminalBuffer.slice(-tailBytes);
|
cleanBuffer = strippedBuffer.slice(-tailBytes);
|
||||||
truncated = true;
|
truncated = true;
|
||||||
// Avoid starting mid-ANSI-escape: find first newline within the first 4KB
|
// Avoid starting mid-ANSI-escape: find first newline within the first 4KB
|
||||||
// and start from there. This prevents xterm.js from parsing a partial escape
|
// and start from there. This prevents xterm.js from parsing a partial escape
|
||||||
@@ -548,7 +587,7 @@ export function registerSessionRoutes(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Full buffer: clean junk before actual Claude content
|
// Full buffer: clean junk before actual Claude content
|
||||||
cleanBuffer = session.terminalBuffer;
|
cleanBuffer = strippedBuffer;
|
||||||
|
|
||||||
// Find where Claude banner starts (has color codes before "Claude")
|
// Find where Claude banner starts (has color codes before "Claude")
|
||||||
const claudeMatch = cleanBuffer.match(CLAUDE_BANNER_PATTERN);
|
const claudeMatch = cleanBuffer.match(CLAUDE_BANNER_PATTERN);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"status": "failed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
Reference in new issue
Block a user