mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
chore: version packages
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
# aicodeman
|
# aicodeman
|
||||||
|
|
||||||
|
## 0.5.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Make buffer size limits configurable via CODEMAN\_\* environment variables (MAX_TERMINAL_BUFFER, TRIM_TERMINAL_TO, MAX_TEXT_OUTPUT, TRIM_TEXT_TO, MAX_MESSAGES), falling back to existing defaults. Allows users with fewer sessions or more RAM to tune buffer sizes without patching source.
|
||||||
|
|
||||||
|
Fix duplicate terminal output on tab switch to busy sessions by clearing the terminal before writing the new buffer.
|
||||||
|
|
||||||
|
Fix stale Ink CUP frames after tab switch by sending Ctrl+L to force a clean redraw.
|
||||||
|
|
||||||
|
Fix mobile CJK input handling: resolve textarea positioning, terminal flicker during composition, and layout overflow on small screens. Improve CJK composition lifecycle with better event handling and fallback flush timers.
|
||||||
|
|
||||||
## 0.5.1
|
## 0.5.1
|
||||||
|
|
||||||
### 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.5.1 (must match `package.json`)
|
**Version**: 0.5.2 (must match `package.json`)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aicodeman",
|
"name": "aicodeman",
|
||||||
"version": "0.5.1",
|
"version": "0.5.2",
|
||||||
"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",
|
||||||
|
|||||||
+223
-152
@@ -64,7 +64,9 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const unicode11Addon = new Unicode11Addon.Unicode11Addon();
|
const unicode11Addon = new Unicode11Addon.Unicode11Addon();
|
||||||
this.terminal.loadAddon(unicode11Addon);
|
this.terminal.loadAddon(unicode11Addon);
|
||||||
this.terminal.unicode.activeVersion = '11';
|
this.terminal.unicode.activeVersion = '11';
|
||||||
} catch (_e) { /* Unicode11 addon failed — default Unicode handling used */ }
|
} catch (_e) {
|
||||||
|
/* Unicode11 addon failed — default Unicode handling used */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.getElementById('terminalContainer');
|
const container = document.getElementById('terminalContainer');
|
||||||
@@ -115,8 +117,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
// On mobile Safari, delay initial fit() to allow layout to settle
|
// On mobile Safari, delay initial fit() to allow layout to settle
|
||||||
// This prevents 0-column terminals caused by fit() running before container is sized
|
// This prevents 0-column terminals caused by fit() running before container is sized
|
||||||
const isMobileSafari = MobileDetection.getDeviceType() === 'mobile' &&
|
const isMobileSafari =
|
||||||
document.body.classList.contains('safari-browser');
|
MobileDetection.getDeviceType() === 'mobile' && document.body.classList.contains('safari-browser');
|
||||||
if (isMobileSafari) {
|
if (isMobileSafari) {
|
||||||
// Wait for layout, then fit multiple times to ensure proper sizing
|
// Wait for layout, then fit multiple times to ensure proper sizing
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -133,11 +135,15 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
// Always use mouse wheel for terminal scrollback, never forward to application.
|
// Always use mouse wheel for terminal scrollback, never forward to application.
|
||||||
// Prevents Claude's Ink UI (plan mode selector) from capturing scroll as option navigation.
|
// Prevents Claude's Ink UI (plan mode selector) from capturing scroll as option navigation.
|
||||||
container.addEventListener('wheel', (ev) => {
|
container.addEventListener(
|
||||||
ev.preventDefault();
|
'wheel',
|
||||||
const lines = Math.round(ev.deltaY / 25) || (ev.deltaY > 0 ? 1 : -1);
|
(ev) => {
|
||||||
this.terminal.scrollLines(lines);
|
ev.preventDefault();
|
||||||
}, { passive: false });
|
const lines = Math.round(ev.deltaY / 25) || (ev.deltaY > 0 ? 1 : -1);
|
||||||
|
this.terminal.scrollLines(lines);
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
|
||||||
// Touch scrolling — use terminal.scrollLines() for all devices.
|
// Touch scrolling — use terminal.scrollLines() for all devices.
|
||||||
// xterm.js DOM renderer doesn't populate xterm-viewport's scroll area,
|
// xterm.js DOM renderer doesn't populate xterm-viewport's scroll area,
|
||||||
@@ -145,8 +151,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
// has nothing to scroll. Instead, convert touch deltas into scrollLines()
|
// has nothing to scroll. Instead, convert touch deltas into scrollLines()
|
||||||
// calls, matching the wheel handler above.
|
// calls, matching the wheel handler above.
|
||||||
{
|
{
|
||||||
const cellHeight = () =>
|
const cellHeight = () => this.terminal._core?._renderService?.dimensions?.css?.cell?.height || 13;
|
||||||
this.terminal._core?._renderService?.dimensions?.css?.cell?.height || 13;
|
|
||||||
let touchLastY = 0;
|
let touchLastY = 0;
|
||||||
let velocity = 0;
|
let velocity = 0;
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
@@ -174,55 +179,65 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
// Accumulate sub-line pixel deltas so slow swipes still scroll
|
// Accumulate sub-line pixel deltas so slow swipes still scroll
|
||||||
let pixelAccum = 0;
|
let pixelAccum = 0;
|
||||||
|
|
||||||
let didScroll = false; // track whether touchmove fired (tap vs scroll)
|
container.addEventListener(
|
||||||
container.addEventListener('touchstart', (ev) => {
|
'touchstart',
|
||||||
if (ev.touches.length === 1) {
|
(ev) => {
|
||||||
touchLastY = ev.touches[0].clientY;
|
if (ev.touches.length === 1) {
|
||||||
|
touchLastY = ev.touches[0].clientY;
|
||||||
|
velocity = 0;
|
||||||
|
pixelAccum = 0;
|
||||||
|
isTouching = true;
|
||||||
|
lastTime = 0;
|
||||||
|
if (scrollFrame) {
|
||||||
|
cancelAnimationFrame(scrollFrame);
|
||||||
|
scrollFrame = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
container.addEventListener(
|
||||||
|
'touchmove',
|
||||||
|
(ev) => {
|
||||||
|
if (ev.touches.length === 1 && isTouching) {
|
||||||
|
const touchY = ev.touches[0].clientY;
|
||||||
|
const delta = touchLastY - touchY; // positive = scroll down
|
||||||
|
pixelAccum += delta;
|
||||||
|
velocity = delta * 1.2;
|
||||||
|
touchLastY = touchY;
|
||||||
|
// Convert accumulated pixels to whole lines
|
||||||
|
const ch = cellHeight();
|
||||||
|
const lines = Math.trunc(pixelAccum / ch);
|
||||||
|
if (lines !== 0) {
|
||||||
|
this.terminal.scrollLines(lines);
|
||||||
|
pixelAccum -= lines * ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
container.addEventListener(
|
||||||
|
'touchend',
|
||||||
|
() => {
|
||||||
|
isTouching = false;
|
||||||
|
if (!scrollFrame && Math.abs(velocity) > 0.3) {
|
||||||
|
scrollFrame = requestAnimationFrame(scrollLoop);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
container.addEventListener(
|
||||||
|
'touchcancel',
|
||||||
|
() => {
|
||||||
|
isTouching = false;
|
||||||
velocity = 0;
|
velocity = 0;
|
||||||
pixelAccum = 0;
|
pixelAccum = 0;
|
||||||
isTouching = true;
|
},
|
||||||
didScroll = false;
|
{ passive: true }
|
||||||
lastTime = 0;
|
);
|
||||||
if (scrollFrame) { cancelAnimationFrame(scrollFrame); scrollFrame = null; }
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
container.addEventListener('touchmove', (ev) => {
|
|
||||||
if (ev.touches.length === 1 && isTouching) {
|
|
||||||
didScroll = true;
|
|
||||||
const touchY = ev.touches[0].clientY;
|
|
||||||
const delta = touchLastY - touchY; // positive = scroll down
|
|
||||||
pixelAccum += delta;
|
|
||||||
velocity = delta * 1.2;
|
|
||||||
touchLastY = touchY;
|
|
||||||
// Convert accumulated pixels to whole lines
|
|
||||||
const ch = cellHeight();
|
|
||||||
const lines = Math.trunc(pixelAccum / ch);
|
|
||||||
if (lines !== 0) {
|
|
||||||
this.terminal.scrollLines(lines);
|
|
||||||
pixelAccum -= lines * ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
container.addEventListener('touchend', () => {
|
|
||||||
isTouching = false;
|
|
||||||
if (!scrollFrame && Math.abs(velocity) > 0.3) {
|
|
||||||
scrollFrame = requestAnimationFrame(scrollLoop);
|
|
||||||
}
|
|
||||||
// Tap (no scroll): refocus xterm's hidden textarea so keyboard input
|
|
||||||
// routes back to the terminal. Without this, a tap on the terminal area
|
|
||||||
// consumes the touch event but xterm's textarea never regains focus.
|
|
||||||
if (!didScroll && this.terminal) {
|
|
||||||
this.terminal.focus();
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
container.addEventListener('touchcancel', () => {
|
|
||||||
isTouching = false;
|
|
||||||
velocity = 0;
|
|
||||||
pixelAccum = 0;
|
|
||||||
}, { passive: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Welcome message
|
// Welcome message
|
||||||
@@ -269,6 +284,22 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
}
|
}
|
||||||
this.flushFlickerBuffer();
|
this.flushFlickerBuffer();
|
||||||
}
|
}
|
||||||
|
// Clear viewport + scrollback for Ink-based sessions before sending SIGWINCH.
|
||||||
|
// fitAddon.fit() reflows content: lines at old width may wrap to more rows,
|
||||||
|
// pushing overflow into scrollback. Ink's cursor-up count is based on the
|
||||||
|
// pre-reflow line count, so ghost renders accumulate in scrollback.
|
||||||
|
// Fix: \x1b[3J (Erase Saved Lines) clears scrollback reflow debris,
|
||||||
|
// then \x1b[H\x1b[2J clears the viewport for a clean Ink redraw.
|
||||||
|
const activeResizeSession = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null;
|
||||||
|
if (
|
||||||
|
activeResizeSession &&
|
||||||
|
activeResizeSession.mode !== 'shell' &&
|
||||||
|
!activeResizeSession._ended &&
|
||||||
|
this.terminal &&
|
||||||
|
this.isTerminalAtBottom()
|
||||||
|
) {
|
||||||
|
this.terminal.write('\x1b[3J\x1b[H\x1b[2J');
|
||||||
|
}
|
||||||
// Skip server resize while mobile keyboard is visible — sending SIGWINCH
|
// Skip server resize while mobile keyboard is visible — sending SIGWINCH
|
||||||
// causes Ink to re-render at the new row count, garbling terminal output.
|
// causes Ink to re-render at the new row count, garbling terminal output.
|
||||||
// Local fit() still runs so xterm knows the viewport size for scrolling.
|
// Local fit() still runs so xterm knows the viewport size for scrolling.
|
||||||
@@ -279,27 +310,12 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const cols = dims ? Math.max(dims.cols, MIN_COLS) : MIN_COLS;
|
const cols = dims ? Math.max(dims.cols, MIN_COLS) : MIN_COLS;
|
||||||
const rows = dims ? Math.max(dims.rows, MIN_ROWS) : MIN_ROWS;
|
const rows = dims ? Math.max(dims.rows, MIN_ROWS) : MIN_ROWS;
|
||||||
// Only send resize if dimensions actually changed
|
// Only send resize if dimensions actually changed
|
||||||
if (!this._lastResizeDims ||
|
if (!this._lastResizeDims || cols !== this._lastResizeDims.cols || rows !== this._lastResizeDims.rows) {
|
||||||
cols !== this._lastResizeDims.cols ||
|
|
||||||
rows !== this._lastResizeDims.rows) {
|
|
||||||
// Clear viewport + scrollback ONLY when dimensions actually change.
|
|
||||||
// fitAddon.fit() reflows content: lines at old width may wrap to more rows,
|
|
||||||
// pushing overflow into scrollback. Ink's cursor-up count is based on the
|
|
||||||
// pre-reflow line count, so ghost renders accumulate in scrollback.
|
|
||||||
// Fix: \x1b[3J (Erase Saved Lines) clears scrollback reflow debris,
|
|
||||||
// then \x1b[H\x1b[2J clears the viewport for a clean Ink redraw.
|
|
||||||
// IMPORTANT: Only clear when we're actually sending SIGWINCH (dims changed).
|
|
||||||
// Clearing without a subsequent Ink redraw leaves the terminal blank.
|
|
||||||
const activeResizeSession = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null;
|
|
||||||
if (activeResizeSession && activeResizeSession.mode !== 'shell' && !activeResizeSession._ended
|
|
||||||
&& this.terminal && this.isTerminalAtBottom()) {
|
|
||||||
this.terminal.write('\x1b[3J\x1b[H\x1b[2J');
|
|
||||||
}
|
|
||||||
this._lastResizeDims = { cols, rows };
|
this._lastResizeDims = { cols, rows };
|
||||||
fetch(`/api/sessions/${this.activeSessionId}/resize`, {
|
fetch(`/api/sessions/${this.activeSessionId}/resize`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ cols, rows })
|
body: JSON.stringify({ cols, rows }),
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,7 +469,11 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (p) {
|
if (p) {
|
||||||
const buf = this.terminal.buffer.active;
|
const buf = this.terminal.buffer.active;
|
||||||
const line = buf.getLine(buf.viewportY + p.row);
|
const line = buf.getLine(buf.viewportY + p.row);
|
||||||
if (line) baseText = line.translateToString(true).slice(p.col + 2).trimEnd();
|
if (line)
|
||||||
|
baseText = line
|
||||||
|
.translateToString(true)
|
||||||
|
.slice(p.col + 2)
|
||||||
|
.trimEnd();
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
this._tabCompletionBaseText = baseText;
|
this._tabCompletionBaseText = baseText;
|
||||||
@@ -468,7 +488,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const selfTab = this;
|
const selfTab = this;
|
||||||
this._tabCompletionFallback = setTimeout(() => {
|
this._tabCompletionFallback = setTimeout(() => {
|
||||||
selfTab._tabCompletionFallback = null;
|
selfTab._tabCompletionFallback = null;
|
||||||
if (!selfTab._tabCompletionSessionId || selfTab._tabCompletionSessionId !== selfTab.activeSessionId) return;
|
if (!selfTab._tabCompletionSessionId || selfTab._tabCompletionSessionId !== selfTab.activeSessionId)
|
||||||
|
return;
|
||||||
const ov = selfTab._localEchoOverlay;
|
const ov = selfTab._localEchoOverlay;
|
||||||
if (!ov || ov.pendingText) return;
|
if (!ov || ov.pendingText) return;
|
||||||
selfTab.terminal.write('', () => {
|
selfTab.terminal.write('', () => {
|
||||||
@@ -567,7 +588,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buffer = self.terminal.buffer.active;
|
const buffer = self.terminal.buffer.active;
|
||||||
const line = buffer.getLine(bufferLineNumber);
|
// provideLinks passes 1-based line number, getLine expects 0-based
|
||||||
|
const line = buffer.getLine(bufferLineNumber - 1);
|
||||||
|
|
||||||
if (!line) {
|
if (!line) {
|
||||||
callback(undefined);
|
callback(undefined);
|
||||||
@@ -584,12 +606,37 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
const links = [];
|
const links = [];
|
||||||
|
|
||||||
|
// Pattern 0: URLs (https://, http://) — matched first so they take priority
|
||||||
|
const urlPattern = /https?:\/\/[^\s"'<>|;&)\]\x00-\x1f]+/g;
|
||||||
|
|
||||||
|
const addUrlLink = (url, matchIndex) => {
|
||||||
|
// Strip trailing punctuation that's likely not part of the URL
|
||||||
|
const cleaned = url.replace(/[.,;:!?)]+$/, '');
|
||||||
|
const startCol = lineText.indexOf(cleaned, matchIndex);
|
||||||
|
if (startCol === -1) return;
|
||||||
|
|
||||||
|
if (links.some((l) => l.range.start.x === startCol + 1)) return;
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
text: cleaned,
|
||||||
|
range: {
|
||||||
|
start: { x: startCol + 1, y: bufferLineNumber },
|
||||||
|
end: { x: startCol + cleaned.length + 1, y: bufferLineNumber },
|
||||||
|
},
|
||||||
|
decorations: { pointerCursor: true, underline: true },
|
||||||
|
activate(_event, text) {
|
||||||
|
window.open(text, '_blank', 'noopener,noreferrer');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Pattern 1: Commands with file paths (tail -f, cat, head, grep pattern, etc.)
|
// Pattern 1: Commands with file paths (tail -f, cat, head, grep pattern, etc.)
|
||||||
// Handles: tail -f /path, grep pattern /path, cat -n /path
|
// Handles: tail -f /path, grep pattern /path, cat -n /path
|
||||||
const cmdPattern = /(tail|cat|head|less|grep|watch|vim|nano)\s+(?:[^\s\/]*\s+)*(\/[^\s"'<>|;&\n\x00-\x1f]+)/g;
|
const cmdPattern = /(tail|cat|head|less|grep|watch|vim|nano)\s+(?:[^\s\/]*\s+)*(\/[^\s"'<>|;&\n\x00-\x1f]+)/g;
|
||||||
|
|
||||||
// Pattern 2: Paths with common extensions
|
// Pattern 2: Paths with common extensions
|
||||||
const extPattern = /(\/(?:home|tmp|var|etc|opt)[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|md|yaml|yml|csv|xml|sh|py|ts|js))\b/g;
|
const extPattern =
|
||||||
|
/(\/(?:home|tmp|var|etc|opt)[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|md|yaml|yml|csv|xml|sh|py|ts|js))\b/g;
|
||||||
|
|
||||||
// Pattern 3: Bash() tool output
|
// Pattern 3: Bash() tool output
|
||||||
const bashPattern = /Bash\([^)]*?(\/(?:home|tmp|var|etc|opt)[^\s"'<>|;&\)\n\x00-\x1f]+)/g;
|
const bashPattern = /Bash\([^)]*?(\/(?:home|tmp|var|etc|opt)[^\s"'<>|;&\)\n\x00-\x1f]+)/g;
|
||||||
@@ -599,27 +646,32 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (startCol === -1) return;
|
if (startCol === -1) return;
|
||||||
|
|
||||||
// Skip if already have link at this position
|
// Skip if already have link at this position
|
||||||
if (links.some(l => l.range.start.x === startCol + 1)) return;
|
if (links.some((l) => l.range.start.x === startCol + 1)) return;
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
text: filePath,
|
text: filePath,
|
||||||
range: {
|
range: {
|
||||||
start: { x: startCol + 1, y: bufferLineNumber }, // 1-based
|
start: { x: startCol + 1, y: bufferLineNumber }, // 1-based
|
||||||
end: { x: startCol + filePath.length + 1, y: bufferLineNumber }
|
end: { x: startCol + filePath.length + 1, y: bufferLineNumber },
|
||||||
},
|
},
|
||||||
decorations: {
|
decorations: {
|
||||||
pointerCursor: true,
|
pointerCursor: true,
|
||||||
underline: true
|
underline: true,
|
||||||
},
|
},
|
||||||
activate(event, text) {
|
activate(event, text) {
|
||||||
self.openLogViewerWindow(text, self.activeSessionId);
|
self.openLogViewerWindow(text, self.activeSessionId);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Match all patterns
|
// Match all patterns — URLs first so they take priority
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
|
urlPattern.lastIndex = 0;
|
||||||
|
while ((match = urlPattern.exec(lineText)) !== null) {
|
||||||
|
addUrlLink(match[0], match.index);
|
||||||
|
}
|
||||||
|
|
||||||
cmdPattern.lastIndex = 0;
|
cmdPattern.lastIndex = 0;
|
||||||
while ((match = cmdPattern.exec(lineText)) !== null) {
|
while ((match = cmdPattern.exec(lineText)) !== null) {
|
||||||
addLink(match[2], match.index);
|
addLink(match[2], match.index);
|
||||||
@@ -636,10 +688,13 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (links.length > 0) {
|
if (links.length > 0) {
|
||||||
console.debug('[LinkProvider] Found links:', links.map(l => l.text));
|
console.debug(
|
||||||
|
'[LinkProvider] Found links:',
|
||||||
|
links.map((l) => l.text)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
callback(links.length > 0 ? links : undefined);
|
callback(links.length > 0 ? links : undefined);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[LinkProvider] File path link provider registered');
|
console.log('[LinkProvider] File path link provider registered');
|
||||||
@@ -705,12 +760,17 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
// Build DOM safely (no innerHTML with user data)
|
// Build DOM safely (no innerHTML with user data)
|
||||||
list.replaceChildren();
|
list.replaceChildren();
|
||||||
for (const s of display) {
|
for (const s of display) {
|
||||||
const size = s.sizeBytes < 1024 ? `${s.sizeBytes}B`
|
const size =
|
||||||
: s.sizeBytes < 1048576 ? `${(s.sizeBytes / 1024).toFixed(0)}K`
|
s.sizeBytes < 1024
|
||||||
: `${(s.sizeBytes / 1048576).toFixed(1)}M`;
|
? `${s.sizeBytes}B`
|
||||||
|
: s.sizeBytes < 1048576
|
||||||
|
? `${(s.sizeBytes / 1024).toFixed(0)}K`
|
||||||
|
: `${(s.sizeBytes / 1048576).toFixed(1)}M`;
|
||||||
const date = new Date(s.lastModified);
|
const date = new Date(s.lastModified);
|
||||||
const timeStr = date.toLocaleDateString('en', { month: 'short', day: 'numeric' })
|
const timeStr =
|
||||||
+ ' ' + date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false });
|
date.toLocaleDateString('en', { month: 'short', day: 'numeric' }) +
|
||||||
|
' ' +
|
||||||
|
date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||||
const shortDir = s.workingDir.replace(/^\/home\/[^/]+\//, '~/');
|
const shortDir = s.workingDir.replace(/^\/home\/[^/]+\//, '~/');
|
||||||
|
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
@@ -773,7 +833,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const createRes = await fetch('/api/sessions', {
|
const createRes = await fetch('/api/sessions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ workingDir, name, resumeSessionId: sessionId })
|
body: JSON.stringify({ workingDir, name, resumeSessionId: sessionId }),
|
||||||
});
|
});
|
||||||
const createData = await createRes.json();
|
const createData = await createRes.json();
|
||||||
if (!createData.success) throw new Error(createData.error);
|
if (!createData.success) throw new Error(createData.error);
|
||||||
@@ -791,7 +851,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// Terminal Rendering
|
// Terminal Rendering
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@@ -836,9 +895,10 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
// Opt-in flicker filter: buffer screen clear patterns (for sessions that enable it)
|
// Opt-in flicker filter: buffer screen clear patterns (for sessions that enable it)
|
||||||
if (flickerFilterEnabled) {
|
if (flickerFilterEnabled) {
|
||||||
const hasScreenClear = data.includes('\x1b[2J') ||
|
const hasScreenClear =
|
||||||
data.includes('\x1b[H\x1b[J') ||
|
data.includes('\x1b[2J') ||
|
||||||
(data.includes('\x1b[H') && data.includes('\x1b[?25l'));
|
data.includes('\x1b[H\x1b[J') ||
|
||||||
|
(data.includes('\x1b[H') && data.includes('\x1b[?25l'));
|
||||||
|
|
||||||
if (hasScreenClear) {
|
if (hasScreenClear) {
|
||||||
this.flickerFilterActive = true;
|
this.flickerFilterActive = true;
|
||||||
@@ -904,47 +964,49 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
* Position is tracked dynamically by _findPrompt() on every render.
|
* Position is tracked dynamically by _findPrompt() on every render.
|
||||||
*/
|
*/
|
||||||
_updateLocalEchoState() {
|
_updateLocalEchoState() {
|
||||||
const settings = this.loadAppSettingsFromStorage();
|
const settings = this.loadAppSettingsFromStorage();
|
||||||
const session = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null;
|
const session = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null;
|
||||||
const echoEnabled = settings.localEchoEnabled ?? MobileDetection.isTouchDevice();
|
const echoEnabled = settings.localEchoEnabled ?? MobileDetection.isTouchDevice();
|
||||||
const shouldEnable = !!(echoEnabled && session);
|
const shouldEnable = !!(echoEnabled && session);
|
||||||
if (this._localEchoEnabled && !shouldEnable) {
|
if (this._localEchoEnabled && !shouldEnable) {
|
||||||
this._localEchoOverlay?.clear();
|
this._localEchoOverlay?.clear();
|
||||||
}
|
}
|
||||||
this._localEchoEnabled = shouldEnable;
|
this._localEchoEnabled = shouldEnable;
|
||||||
|
|
||||||
// Swap prompt finder based on session mode
|
// Swap prompt finder based on session mode
|
||||||
if (this._localEchoOverlay && session) {
|
if (this._localEchoOverlay && session) {
|
||||||
if (session.mode === 'opencode') {
|
if (session.mode === 'opencode') {
|
||||||
// OpenCode (Bubble Tea TUI): find the ┃ border on the cursor's row.
|
// OpenCode (Bubble Tea TUI): find the ┃ border on the cursor's row.
|
||||||
// The input area is "┃ <text>" — the ┃ is the anchor, offset 3 skips "┃ ".
|
// The input area is "┃ <text>" — the ┃ is the anchor, offset 3 skips "┃ ".
|
||||||
// We use the cursor row (cursorY) to find the right line, then scan for ┃.
|
// We use the cursor row (cursorY) to find the right line, then scan for ┃.
|
||||||
this._localEchoOverlay.setPrompt({
|
this._localEchoOverlay.setPrompt({
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
offset: 3,
|
offset: 3,
|
||||||
find: (terminal) => {
|
find: (terminal) => {
|
||||||
try {
|
try {
|
||||||
const buf = terminal.buffer.active;
|
const buf = terminal.buffer.active;
|
||||||
const row = buf.cursorY;
|
const row = buf.cursorY;
|
||||||
const line = buf.getLine(buf.viewportY + row);
|
const line = buf.getLine(buf.viewportY + row);
|
||||||
if (!line) return null;
|
if (!line) return null;
|
||||||
const text = line.translateToString(true);
|
const text = line.translateToString(true);
|
||||||
const idx = text.indexOf('\u2503'); // ┃ (BOX DRAWINGS HEAVY VERTICAL)
|
const idx = text.indexOf('\u2503'); // ┃ (BOX DRAWINGS HEAVY VERTICAL)
|
||||||
if (idx >= 0) return { row, col: idx };
|
if (idx >= 0) return { row, col: idx };
|
||||||
return null;
|
return null;
|
||||||
} catch { return null; }
|
} catch {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
} else if (session.mode === 'shell') {
|
});
|
||||||
// Shell mode: the shell provides its own PTY echo so the overlay isn't needed.
|
} else if (session.mode === 'shell') {
|
||||||
// Disable it by clearing any pending text.
|
// Shell mode: the shell provides its own PTY echo so the overlay isn't needed.
|
||||||
this._localEchoOverlay.clear();
|
// Disable it by clearing any pending text.
|
||||||
this._localEchoEnabled = false;
|
this._localEchoOverlay.clear();
|
||||||
} else {
|
this._localEchoEnabled = false;
|
||||||
// Claude Code: scan for ❯ prompt character
|
} else {
|
||||||
this._localEchoOverlay.setPrompt({ type: 'character', char: '\u276f', offset: 2 });
|
// Claude Code: scan for ❯ prompt character
|
||||||
}
|
this._localEchoOverlay.setPrompt({ type: 'character', char: '\u276f', offset: 2 });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -961,7 +1023,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const joined = this.pendingWrites.join('');
|
const joined = this.pendingWrites.join('');
|
||||||
this.pendingWrites = [];
|
this.pendingWrites = [];
|
||||||
const _joinedLen = joined.length;
|
const _joinedLen = joined.length;
|
||||||
if (_joinedLen > 16384) _crashDiag.log(`FLUSH: ${(_joinedLen/1024).toFixed(0)}KB`);
|
if (_joinedLen > 16384) _crashDiag.log(`FLUSH: ${(_joinedLen / 1024).toFixed(0)}KB`);
|
||||||
|
|
||||||
// Per-frame byte budget to prevent main thread blocking.
|
// Per-frame byte budget to prevent main thread blocking.
|
||||||
// Large writes (141KB+) can freeze Chrome for 2+ minutes.
|
// Large writes (141KB+) can freeze Chrome for 2+ minutes.
|
||||||
@@ -985,7 +1047,10 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
}
|
}
|
||||||
const bytesThisFrame = deferred ? MAX_FRAME_BYTES : _joinedLen;
|
const bytesThisFrame = deferred ? MAX_FRAME_BYTES : _joinedLen;
|
||||||
const _dt = performance.now() - _t0;
|
const _dt = performance.now() - _t0;
|
||||||
if (_dt > 100 || deferred) console.warn(`[CRASH-DIAG] flushPendingWrites: ${_dt.toFixed(0)}ms, ${(bytesThisFrame/1024).toFixed(0)}KB written${deferred ? ', rest deferred' : ''} (total ${(_joinedLen/1024).toFixed(0)}KB)`);
|
if (_dt > 100 || deferred)
|
||||||
|
console.warn(
|
||||||
|
`[CRASH-DIAG] flushPendingWrites: ${_dt.toFixed(0)}ms, ${(bytesThisFrame / 1024).toFixed(0)}KB written${deferred ? ', rest deferred' : ''} (total ${(_joinedLen / 1024).toFixed(0)}KB)`
|
||||||
|
);
|
||||||
|
|
||||||
// Sticky scroll: if user was at bottom, keep them there after new output
|
// Sticky scroll: if user was at bottom, keep them there after new output
|
||||||
if (this._wasAtBottomBeforeWrite) {
|
if (this._wasAtBottomBeforeWrite) {
|
||||||
@@ -1002,8 +1067,12 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
// Use terminal.write('', callback) to defer detection until xterm.js
|
// Use terminal.write('', callback) to defer detection until xterm.js
|
||||||
// finishes processing ALL queued writes — direct buffer reads after
|
// finishes processing ALL queued writes — direct buffer reads after
|
||||||
// terminal.write(data) can miss text if xterm processes asynchronously.
|
// terminal.write(data) can miss text if xterm processes asynchronously.
|
||||||
if (this._tabCompletionSessionId && this._tabCompletionSessionId === this.activeSessionId
|
if (
|
||||||
&& this._localEchoOverlay && !this._localEchoOverlay.pendingText) {
|
this._tabCompletionSessionId &&
|
||||||
|
this._tabCompletionSessionId === this.activeSessionId &&
|
||||||
|
this._localEchoOverlay &&
|
||||||
|
!this._localEchoOverlay.pendingText
|
||||||
|
) {
|
||||||
const overlay = this._localEchoOverlay;
|
const overlay = this._localEchoOverlay;
|
||||||
const self = this;
|
const self = this;
|
||||||
this.terminal.write('', () => {
|
this.terminal.write('', () => {
|
||||||
@@ -1024,7 +1093,10 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
self._tabCompletionSessionId = null;
|
self._tabCompletionSessionId = null;
|
||||||
self._tabCompletionRetries = 0;
|
self._tabCompletionRetries = 0;
|
||||||
self._tabCompletionBaseText = null;
|
self._tabCompletionBaseText = null;
|
||||||
if (self._tabCompletionFallback) { clearTimeout(self._tabCompletionFallback); self._tabCompletionFallback = null; }
|
if (self._tabCompletionFallback) {
|
||||||
|
clearTimeout(self._tabCompletionFallback);
|
||||||
|
self._tabCompletionFallback = null;
|
||||||
|
}
|
||||||
overlay.rerender();
|
overlay.rerender();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1096,7 +1168,9 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
if (offset >= cleanBuffer.length) {
|
if (offset >= cleanBuffer.length) {
|
||||||
const _totalMs = performance.now() - _chunkStart;
|
const _totalMs = performance.now() - _chunkStart;
|
||||||
console.log(`[CRASH-DIAG] chunkedTerminalWrite complete: ${cleanBuffer.length} bytes in ${_chunkCount} chunks, ${_totalMs.toFixed(0)}ms total`);
|
console.log(
|
||||||
|
`[CRASH-DIAG] chunkedTerminalWrite complete: ${cleanBuffer.length} bytes in ${_chunkCount} chunks, ${_totalMs.toFixed(0)}ms total`
|
||||||
|
);
|
||||||
// Wait one more frame for xterm to finish rendering before resolving
|
// Wait one more frame for xterm to finish rendering before resolving
|
||||||
requestAnimationFrame(finish);
|
requestAnimationFrame(finish);
|
||||||
return;
|
return;
|
||||||
@@ -1107,7 +1181,10 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this.terminal.write(chunk);
|
this.terminal.write(chunk);
|
||||||
const _cdt = performance.now() - _ct0;
|
const _cdt = performance.now() - _ct0;
|
||||||
_chunkCount++;
|
_chunkCount++;
|
||||||
if (_cdt > 50) console.warn(`[CRASH-DIAG] chunk #${_chunkCount} write took ${_cdt.toFixed(0)}ms (${chunk.length} bytes at offset ${offset})`);
|
if (_cdt > 50)
|
||||||
|
console.warn(
|
||||||
|
`[CRASH-DIAG] chunk #${_chunkCount} write took ${_cdt.toFixed(0)}ms (${chunk.length} bytes at offset ${offset})`
|
||||||
|
);
|
||||||
offset += chunkSize;
|
offset += chunkSize;
|
||||||
|
|
||||||
// Schedule next chunk on next frame
|
// Schedule next chunk on next frame
|
||||||
@@ -1134,7 +1211,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this._loadBufferQueue = null;
|
this._loadBufferQueue = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// Terminal Controls
|
// Terminal Controls
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@@ -1168,7 +1244,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
await fetch(`/api/sessions/${this.activeSessionId}/input`, {
|
await fetch(`/api/sessions/${this.activeSessionId}/input`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ input: '\x0c' })
|
body: JSON.stringify({ input: '\x0c' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.showToast(`Terminal restored to ${dims.cols}x${dims.rows}`, 'success');
|
this.showToast(`Terminal restored to ${dims.cols}x${dims.rows}`, 'success');
|
||||||
@@ -1195,7 +1271,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
fetch(`/api/sessions/${sessionId}/input`, {
|
fetch(`/api/sessions/${sessionId}/input`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ input: '\x0c' })
|
body: JSON.stringify({ input: '\x0c' }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1257,7 +1333,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (!dims) return null;
|
if (!dims) return null;
|
||||||
return {
|
return {
|
||||||
cols: Math.max(dims.cols, MIN_COLS),
|
cols: Math.max(dims.cols, MIN_COLS),
|
||||||
rows: Math.max(dims.rows, MIN_ROWS)
|
rows: Math.max(dims.rows, MIN_ROWS),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1272,10 +1348,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (this.fitAddon) this.fitAddon.fit();
|
if (this.fitAddon) this.fitAddon.fit();
|
||||||
const dims = this.getTerminalDimensions();
|
const dims = this.getTerminalDimensions();
|
||||||
if (!dims) return;
|
if (!dims) return;
|
||||||
// Update _lastResizeDims so the throttledResize handler won't redundantly
|
|
||||||
// clear the terminal for the same dimensions (which would blank the screen
|
|
||||||
// without a subsequent Ink redraw to repaint it).
|
|
||||||
this._lastResizeDims = { cols: dims.cols, rows: dims.rows };
|
|
||||||
// Fast path: WebSocket resize
|
// Fast path: WebSocket resize
|
||||||
if (this._wsReady && this._wsSessionId === sessionId) {
|
if (this._wsReady && this._wsSessionId === sessionId) {
|
||||||
try {
|
try {
|
||||||
@@ -1288,7 +1360,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
await fetch(`/api/sessions/${sessionId}/resize`, {
|
await fetch(`/api/sessions/${sessionId}/resize`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(dims)
|
body: JSON.stringify(dims),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1302,11 +1374,10 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
await fetch(`/api/sessions/${this.activeSessionId}/input`, {
|
await fetch(`/api/sessions/${this.activeSessionId}/input`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ input, useMux: true })
|
body: JSON.stringify({ input, useMux: true }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// Directory Input
|
// Directory Input
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user