Batch release exp-v0.52.234 — O_BINARY (#7077), native-control font (#6871), mobile nav order (#6909) (#7099)
* fix(navigation): preserve mobile action mirror order Reposition stale extension action mirrors during synchronization without moving already-correct elements, preserving native tab order and keyboard focus. Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Assisted-by: Claude Code:claude-opus-5 * fix(typography): route native controls through UI font Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Assisted-by: Claude Code:claude-opus-5 * fix(windows): open anchored files in binary mode * docs(changelog): note #7077 O_BINARY, #6871 native-control font, #6909 mobile nav order --------- Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com> Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com> Co-authored-by: n <a@n>
This commit is contained in:
7 files changed
+224
-8
No files matched your search
@@ -5,6 +5,12 @@
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **Binary files in the workspace no longer truncate when served on Windows.** The shared anchored file-open helper omitted the platform's `O_BINARY` flag, so on Windows a binary workspace file (video, image, archive) was read in text mode and truncated at its first `0x1A` (Ctrl-Z) byte — a 5.2 MB video could arrive as a few hundred bytes, breaking Range requests and playback. Non-directory anchored opens now set `O_BINARY` where the platform provides it (a no-op on Linux/macOS). Thanks @allenliang2022. (#7077)
|
||||||
|
|
||||||
|
- **Unstyled native form controls now use the UI font.** Bare `button`, `input`, `select`, and `textarea` elements fell back to the browser default font instead of the app's UI font, and the markdown table filter lost the UI font after its `font: inherit` shorthand. Both now route through `--font-ui`; mono, conversation, and skin-scoped controls are unchanged. Thanks @starship-s. (#6871)
|
||||||
|
|
||||||
|
- **Mobile navigation action shortcuts keep their rail order.** In the mobile sidebar, mirrored navigation actions could land out of order relative to the desktop rail after a native-tab or profile reorder. They now reconcile into rail-source order with no needless DOM moves. Thanks @starship-s. (#6909)
|
||||||
|
|
||||||
- **The Workspace Artifacts panel no longer keeps a stale entry after a session recovers, and no longer flashes another session's artifacts into the panel you're reading.** After a settled-session recovery the Artifacts panel could keep showing a moved or deleted artifact as a dead link until you manually switched sessions, and a background session finishing could momentarily repaint the foreground session's panel. Artifact re-projection is now scoped to the current session and pane (a background session's completion no longer touches the panel you're viewing), and the current session's panel refreshes correctly after recovery. Thanks @rodboev. (#6867)
|
- **The Workspace Artifacts panel no longer keeps a stale entry after a session recovers, and no longer flashes another session's artifacts into the panel you're reading.** After a settled-session recovery the Artifacts panel could keep showing a moved or deleted artifact as a dead link until you manually switched sessions, and a background session finishing could momentarily repaint the foreground session's panel. Artifact re-projection is now scoped to the current session and pane (a background session's completion no longer touches the panel you're viewing), and the current session's panel refreshes correctly after recovery. Thanks @rodboev. (#6867)
|
||||||
|
|
||||||
- **Kanban task and board dialogs stay usable in short windows.** On a short window (short-wide landscape, small laptop, or a phone in landscape) the Kanban create/edit-task and create-board dialogs were taller than the viewport with no height cap and no internal scroll, so the dialog overflowed both the top and bottom edges and its action buttons (Create / Save / Cancel) were unreachable. The dialogs now cap their height to the viewport (`calc(100dvh - 48px)`, with a `100vh` fallback) and scroll their content internally, and the overlay uses safe centering so the top stays reachable when the dialog is taller than the window. Normal-height dialogs are unchanged. Thanks @rodboev. (#6906)
|
- **Kanban task and board dialogs stay usable in short windows.** On a short window (short-wide landscape, small laptop, or a phone in landscape) the Kanban create/edit-task and create-board dialogs were taller than the viewport with no height cap and no internal scroll, so the dialog overflowed both the top and bottom edges and its action buttons (Create / Save / Cancel) were unreachable. The dialogs now cap their height to the viewport (`calc(100dvh - 48px)`, with a `100vh` fallback) and scroll their content internally, and the overlay uses safe centering so the top stays reachable when the dialog is taller than the window. Normal-height dialogs are unchanged. Thanks @rodboev. (#6906)
|
||||||
|
|||||||
+11
-2
@@ -1016,6 +1016,7 @@ def safe_resolve_ws(root: Path, requested: str) -> Path:
|
|||||||
_DIR_FD_OK = os.open in getattr(os, "supports_dir_fd", set())
|
_DIR_FD_OK = os.open in getattr(os, "supports_dir_fd", set())
|
||||||
_O_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
|
_O_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
|
||||||
_O_DIRECTORY = getattr(os, "O_DIRECTORY", 0)
|
_O_DIRECTORY = getattr(os, "O_DIRECTORY", 0)
|
||||||
|
_O_BINARY = getattr(os, "O_BINARY", 0)
|
||||||
|
|
||||||
|
|
||||||
def open_anchored_fd(workspace: Path, target: Path, *, want_dir: bool) -> int:
|
def open_anchored_fd(workspace: Path, target: Path, *, want_dir: bool) -> int:
|
||||||
@@ -1037,7 +1038,11 @@ def open_anchored_fd(workspace: Path, target: Path, *, want_dir: bool) -> int:
|
|||||||
# Windows / no openat: fall back to a plain pathname open. No new race
|
# Windows / no openat: fall back to a plain pathname open. No new race
|
||||||
# protection, but no regression vs the prior path-based behaviour, and
|
# protection, but no regression vs the prior path-based behaviour, and
|
||||||
# symlink creation needs admin on Windows anyway.
|
# symlink creation needs admin on Windows anyway.
|
||||||
flags = os.O_RDONLY | (_O_DIRECTORY if want_dir else 0) | _O_NOFOLLOW
|
flags = (
|
||||||
|
os.O_RDONLY
|
||||||
|
| (_O_DIRECTORY if want_dir else _O_BINARY)
|
||||||
|
| _O_NOFOLLOW
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return os.open(str(target), flags)
|
return os.open(str(target), flags)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -1052,7 +1057,11 @@ def open_anchored_fd(workspace: Path, target: Path, *, want_dir: bool) -> int:
|
|||||||
for i, part in enumerate(rel_parts):
|
for i, part in enumerate(rel_parts):
|
||||||
is_last = i == len(rel_parts) - 1
|
is_last = i == len(rel_parts) - 1
|
||||||
want_directory = (not is_last) or want_dir
|
want_directory = (not is_last) or want_dir
|
||||||
flags = os.O_RDONLY | _O_NOFOLLOW | (_O_DIRECTORY if want_directory else 0)
|
flags = (
|
||||||
|
os.O_RDONLY
|
||||||
|
| _O_NOFOLLOW
|
||||||
|
| (_O_DIRECTORY if want_directory else _O_BINARY)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
nfd = os.open(part, flags, dir_fd=fd)
|
nfd = os.open(part, flags, dir_fd=fd)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|||||||
+2
-1
@@ -26,6 +26,7 @@
|
|||||||
--border-subtle:rgba(0,0,0,.08);--border-muted:rgba(0,0,0,.12);
|
--border-subtle:rgba(0,0,0,.08);--border-muted:rgba(0,0,0,.12);
|
||||||
font-family:var(--font-ui);font-size:14px;line-height:1.6;
|
font-family:var(--font-ui);font-size:14px;line-height:1.6;
|
||||||
}
|
}
|
||||||
|
button,input,select,textarea{font-family:var(--font-ui);}
|
||||||
/* ── Font size modifiers ── */
|
/* ── Font size modifiers ── */
|
||||||
/* ── Font size preference: scale key UI text elements ── */
|
/* ── Font size preference: scale key UI text elements ── */
|
||||||
/* Default is 14px (no attribute needed). Small=12px, Large=16px, Extra Large=18px. */
|
/* Default is 14px (no attribute needed). Small=12px, Large=16px, Extra Large=18px. */
|
||||||
@@ -2415,7 +2416,7 @@
|
|||||||
.msg-body th{background:rgba(255,255,255,.07);padding:6px 10px;text-align:left;font-weight:600;border:1px solid var(--border2);}
|
.msg-body th{background:rgba(255,255,255,.07);padding:6px 10px;text-align:left;font-weight:600;border:1px solid var(--border2);}
|
||||||
.msg-body td{padding:5px 10px;border:1px solid rgba(255,255,255,.06);}
|
.msg-body td{padding:5px 10px;border:1px solid rgba(255,255,255,.06);}
|
||||||
.msg-body tr:nth-child(even){background:rgba(255,255,255,.03);}
|
.msg-body tr:nth-child(even){background:rgba(255,255,255,.03);}
|
||||||
.markdown-table-filter{display:block;width:min(260px,100%);margin:8px 0 4px;padding:5px 8px;border:1px solid var(--border2);border-radius:6px;background:var(--input-bg);color:var(--text);font:inherit;font-size:12px;}
|
.markdown-table-filter{display:block;width:min(260px,100%);margin:8px 0 4px;padding:5px 8px;border:1px solid var(--border2);border-radius:6px;background:var(--input-bg);color:var(--text);font:inherit;font-family:var(--font-ui);font-size:12px;}
|
||||||
.markdown-table-filter:focus{outline:1px solid var(--accent);outline-offset:1px;}
|
.markdown-table-filter:focus{outline:1px solid var(--accent);outline-offset:1px;}
|
||||||
.markdown-table-sort{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:20px;padding:0;border:0;background:transparent;color:inherit;font:inherit;font-weight:inherit;text-align:left;cursor:pointer;}
|
.markdown-table-sort{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:20px;padding:0;border:0;background:transparent;color:inherit;font:inherit;font-weight:inherit;text-align:left;cursor:pointer;}
|
||||||
.markdown-table-sort:focus-visible{outline:1px solid var(--accent);outline-offset:2px;}
|
.markdown-table-sort:focus-visible{outline:1px solid var(--accent);outline-offset:2px;}
|
||||||
|
|||||||
+5
-3
@@ -1774,13 +1774,15 @@ function _syncNavActionMirrors(){
|
|||||||
const rail=document.querySelector('.rail');
|
const rail=document.querySelector('.rail');
|
||||||
const sidebar=document.querySelector('.sidebar-nav');
|
const sidebar=document.querySelector('.sidebar-nav');
|
||||||
if(!rail||!sidebar)return;
|
if(!rail||!sidebar)return;
|
||||||
|
const anchor=sidebar.querySelector('.dashboard-link,[data-dashboard-link]')||sidebar.querySelector('[data-panel="logs"]');
|
||||||
const sources=Array.from(rail.querySelectorAll('.nav-tab:not([data-panel]):not([data-dashboard-link])')).filter(source=>source.id);
|
const sources=Array.from(rail.querySelectorAll('.nav-tab:not([data-panel]):not([data-dashboard-link])')).filter(source=>source.id);
|
||||||
const mirrors=Array.from(sidebar.querySelectorAll('[data-nav-action-mirror]'));
|
const mirrors=Array.from(sidebar.querySelectorAll('[data-nav-action-mirror]'));
|
||||||
const sourceIds=new Set(sources.map(source=>source.id));
|
const sourceIds=new Set(sources.map(source=>source.id));
|
||||||
mirrors.forEach(mirror=>{
|
mirrors.forEach(mirror=>{
|
||||||
if(!sourceIds.has(mirror.getAttribute('data-nav-action-mirror')))mirror.remove();
|
if(!sourceIds.has(mirror.getAttribute('data-nav-action-mirror')))mirror.remove();
|
||||||
});
|
});
|
||||||
sources.forEach(source=>{
|
let next=anchor||null;
|
||||||
|
sources.slice().reverse().forEach(source=>{
|
||||||
const sourceVisible=(()=>{
|
const sourceVisible=(()=>{
|
||||||
if(source.hidden||source.getAttribute('aria-hidden')==='true')return false;
|
if(source.hidden||source.getAttribute('aria-hidden')==='true')return false;
|
||||||
if(source.classList.contains('nav-tab-hidden'))return false;
|
if(source.classList.contains('nav-tab-hidden'))return false;
|
||||||
@@ -1804,12 +1806,12 @@ function _syncNavActionMirrors(){
|
|||||||
if(mirror._navActionSource)mirror._navActionSource.click();
|
if(mirror._navActionSource)mirror._navActionSource.click();
|
||||||
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
|
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
|
||||||
});
|
});
|
||||||
const anchor=sidebar.querySelector('.dashboard-link,[data-dashboard-link]')||sidebar.querySelector('[data-panel="logs"]');
|
|
||||||
sidebar.insertBefore(mirror,anchor||null);
|
|
||||||
}else{
|
}else{
|
||||||
mirror.innerHTML=source.innerHTML;
|
mirror.innerHTML=source.innerHTML;
|
||||||
_stripInlineEventHandlers(mirror);
|
_stripInlineEventHandlers(mirror);
|
||||||
}
|
}
|
||||||
|
if(mirror.parentNode!==sidebar||mirror.nextElementSibling!==next)sidebar.insertBefore(mirror,next);
|
||||||
|
next=mirror;
|
||||||
mirror._navActionSource=source;
|
mirror._navActionSource=source;
|
||||||
mirror.classList.toggle('nav-action-visible',sourceVisible);
|
mirror.classList.toggle('nav-action-visible',sourceVisible);
|
||||||
const label=source.getAttribute('data-tooltip')||source.getAttribute('aria-label')||'';
|
const label=source.getAttribute('data-tooltip')||source.getAttribute('aria-label')||'';
|
||||||
|
|||||||
@@ -462,7 +462,17 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav():
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
appendChild(child) { child.parentNode = this; this.children.push(child); return child; }
|
appendChild(child) { child.parentNode = this; this.children.push(child); return child; }
|
||||||
|
get nextElementSibling() {
|
||||||
|
if (!this.parentNode) return null;
|
||||||
|
const index = this.parentNode.children.indexOf(this);
|
||||||
|
return index >= 0 ? this.parentNode.children[index + 1] || null : null;
|
||||||
|
}
|
||||||
insertBefore(child, anchor) {
|
insertBefore(child, anchor) {
|
||||||
|
if (child.parentNode) this.insertBeforeMoves = (this.insertBeforeMoves || 0) + 1;
|
||||||
|
if (child.parentNode) {
|
||||||
|
const currentIdx = child.parentNode.children.indexOf(child);
|
||||||
|
if (currentIdx >= 0) child.parentNode.children.splice(currentIdx, 1);
|
||||||
|
}
|
||||||
const idx = anchor ? this.children.indexOf(anchor) : -1;
|
const idx = anchor ? this.children.indexOf(anchor) : -1;
|
||||||
child.parentNode = this;
|
child.parentNode = this;
|
||||||
if (idx >= 0) this.children.splice(idx, 0, child);
|
if (idx >= 0) this.children.splice(idx, 0, child);
|
||||||
@@ -507,6 +517,7 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav():
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
querySelector(sel) {
|
querySelector(sel) {
|
||||||
|
if (sel.startsWith('.nav-tab[data-panel="')) return this.children.find(el => el.classList.contains('nav-tab') && el.getAttribute('data-panel') === sel.slice(21, -2)) || null;
|
||||||
if (sel.startsWith('[data-nav-action-mirror="')) {
|
if (sel.startsWith('[data-nav-action-mirror="')) {
|
||||||
const id = sel.slice(25, -2);
|
const id = sel.slice(25, -2);
|
||||||
return this.children.find(el => el.getAttribute('data-nav-action-mirror') === id) || null;
|
return this.children.find(el => el.getAttribute('data-nav-action-mirror') === id) || null;
|
||||||
@@ -520,7 +531,10 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav():
|
|||||||
const rail = new El('nav');
|
const rail = new El('nav');
|
||||||
const sidebar = new El('div');
|
const sidebar = new El('div');
|
||||||
const source = new El('button');
|
const source = new El('button');
|
||||||
|
const source2 = new El('button');
|
||||||
const dashboard = new El('button');
|
const dashboard = new El('button');
|
||||||
|
const chatPanel = new El('button');
|
||||||
|
const settingsPanel = new El('button');
|
||||||
let clicked = 0;
|
let clicked = 0;
|
||||||
let sidebarClosed = 0;
|
let sidebarClosed = 0;
|
||||||
source.id = 'hwxThemeCreatorRailBtn';
|
source.id = 'hwxThemeCreatorRailBtn';
|
||||||
@@ -530,8 +544,21 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav():
|
|||||||
source.onclick = () => { clicked += 100; };
|
source.onclick = () => { clicked += 100; };
|
||||||
source.innerHTML = '<svg></svg>';
|
source.innerHTML = '<svg></svg>';
|
||||||
source.click = () => { clicked += 1; };
|
source.click = () => { clicked += 1; };
|
||||||
|
source2.id = 'hwxTypographyRailBtn';
|
||||||
|
source2.classList.add('rail-btn', 'nav-tab', 'has-tooltip');
|
||||||
|
source2.setAttribute('data-tooltip', 'Typography');
|
||||||
|
source2.innerHTML = '<svg></svg>';
|
||||||
|
chatPanel.id = 'chatMobileBtn';
|
||||||
|
chatPanel.classList.add('nav-tab');
|
||||||
|
chatPanel.setAttribute('data-panel', 'chat');
|
||||||
|
settingsPanel.id = 'settingsMobileBtn';
|
||||||
|
settingsPanel.classList.add('nav-tab');
|
||||||
|
settingsPanel.setAttribute('data-panel', 'settings');
|
||||||
dashboard.classList.add('dashboard-link');
|
dashboard.classList.add('dashboard-link');
|
||||||
|
dashboard.id = 'dashboardMobileBtn';
|
||||||
|
sidebar.appendChild(chatPanel);
|
||||||
sidebar.appendChild(dashboard);
|
sidebar.appendChild(dashboard);
|
||||||
|
sidebar.appendChild(settingsPanel);
|
||||||
let observerCallback = null;
|
let observerCallback = null;
|
||||||
let observedRail = null;
|
let observedRail = null;
|
||||||
let observedOptions = null;
|
let observedOptions = null;
|
||||||
@@ -561,17 +588,53 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav():
|
|||||||
const helpers = Function(extractFn('_stripInlineEventHandlers') + '\\n' + extractFn('_syncNavActionMirrors') + '\\n' + extractFn('_initNavActionMirrors') + '; return { _initNavActionMirrors };')();
|
const helpers = Function(extractFn('_stripInlineEventHandlers') + '\\n' + extractFn('_syncNavActionMirrors') + '\\n' + extractFn('_initNavActionMirrors') + '; return { _initNavActionMirrors };')();
|
||||||
helpers._initNavActionMirrors();
|
helpers._initNavActionMirrors();
|
||||||
rail.appendChild(source);
|
rail.appendChild(source);
|
||||||
|
rail.appendChild(source2);
|
||||||
observerCallback();
|
observerCallback();
|
||||||
const mirror = sidebar.children.find(el => el.getAttribute('data-nav-action-mirror') === 'hwxThemeCreatorRailBtn');
|
const mirror = sidebar.children.find(el => el.getAttribute('data-nav-action-mirror') === 'hwxThemeCreatorRailBtn');
|
||||||
|
const mirror2 = sidebar.children.find(el => el.getAttribute('data-nav-action-mirror') === 'hwxTypographyRailBtn');
|
||||||
source.hidden = true;
|
source.hidden = true;
|
||||||
observerCallback();
|
observerCallback();
|
||||||
const mirrorHiddenWhenSourceHidden = !mirror.classList.contains('nav-action-visible');
|
const mirrorHiddenWhenSourceHidden = !mirror.classList.contains('nav-action-visible');
|
||||||
source.hidden = false;
|
source.hidden = false;
|
||||||
observerCallback();
|
observerCallback();
|
||||||
mirror.click();
|
mirror.click();
|
||||||
const mirrorBeforeDashboard = sidebar.children[0] === mirror;
|
const mirrorBeforeDashboard = sidebar.children[1] === mirror && sidebar.children[2] === mirror2 && sidebar.children[3] === dashboard;
|
||||||
|
const initialOrder = sidebar.children.map(el => el.id);
|
||||||
|
const tasksPanel = new El('button');
|
||||||
|
tasksPanel.id = 'tasksMobileBtn';
|
||||||
|
tasksPanel.classList.add('nav-tab');
|
||||||
|
tasksPanel.setAttribute('data-panel', 'tasks');
|
||||||
|
const logsPanel = new El('button');
|
||||||
|
logsPanel.id = 'logsMobileBtn';
|
||||||
|
logsPanel.classList.add('nav-tab');
|
||||||
|
logsPanel.setAttribute('data-panel', 'logs');
|
||||||
|
sidebar.appendChild(tasksPanel);
|
||||||
|
sidebar.appendChild(logsPanel);
|
||||||
|
['tasks', 'logs'].forEach(panel => {
|
||||||
|
const node = sidebar.querySelector('.nav-tab[data-panel="' + panel + '"]');
|
||||||
|
if (node) sidebar.insertBefore(node, dashboard);
|
||||||
|
});
|
||||||
|
const preSyncOrder = sidebar.children.map(el => el.id);
|
||||||
|
const movesBeforeReconcile = sidebar.insertBeforeMoves || 0;
|
||||||
|
observerCallback();
|
||||||
|
const reconciledOrder = sidebar.children.map(el => el.id);
|
||||||
|
const reconciliationMoves = (sidebar.insertBeforeMoves || 0) - movesBeforeReconcile;
|
||||||
|
const movesBeforeNoOpSync = sidebar.insertBeforeMoves || 0;
|
||||||
|
observerCallback();
|
||||||
|
const noOpSyncMoves = (sidebar.insertBeforeMoves || 0) - movesBeforeNoOpSync;
|
||||||
source.remove();
|
source.remove();
|
||||||
observerCallback();
|
observerCallback();
|
||||||
|
const mirrorRemoved = !sidebar.children.some(el => el.getAttribute('data-nav-action-mirror') === 'hwxThemeCreatorRailBtn');
|
||||||
|
dashboard.remove();
|
||||||
|
logsPanel.remove();
|
||||||
|
const source3 = new El('button');
|
||||||
|
source3.id = 'hwxNoAnchorRailBtn';
|
||||||
|
source3.classList.add('rail-btn', 'nav-tab');
|
||||||
|
source3.setAttribute('aria-label', 'No anchor');
|
||||||
|
rail.appendChild(source3);
|
||||||
|
observerCallback();
|
||||||
|
const noAnchorMirror = sidebar.children.find(el => el.getAttribute('data-nav-action-mirror') === 'hwxNoAnchorRailBtn');
|
||||||
|
const noAnchorMirrorAppended = noAnchorMirror?.parentNode === sidebar && sidebar.children.at(-1) === noAnchorMirror;
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
observerArmed: observedRail === rail,
|
observerArmed: observedRail === rail,
|
||||||
observerAttributes: !!(observedOptions && observedOptions.attributes),
|
observerAttributes: !!(observedOptions && observedOptions.attributes),
|
||||||
@@ -584,7 +647,13 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav():
|
|||||||
mirrorOnclickAttribute: mirror.getAttribute('onclick'),
|
mirrorOnclickAttribute: mirror.getAttribute('onclick'),
|
||||||
mirrorOnclickProperty: mirror.onclick === null,
|
mirrorOnclickProperty: mirror.onclick === null,
|
||||||
mirrorBeforeDashboard,
|
mirrorBeforeDashboard,
|
||||||
mirrorRemoved: !sidebar.children.some(el => el.getAttribute('data-nav-action-mirror') === 'hwxThemeCreatorRailBtn'),
|
initialOrder,
|
||||||
|
preSyncOrder,
|
||||||
|
reconciledOrder,
|
||||||
|
reconciliationMoves,
|
||||||
|
noOpSyncMoves,
|
||||||
|
mirrorRemoved,
|
||||||
|
noAnchorMirrorAppended,
|
||||||
clicked,
|
clicked,
|
||||||
sidebarClosed,
|
sidebarClosed,
|
||||||
}));
|
}));
|
||||||
@@ -610,7 +679,35 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav():
|
|||||||
"mirrorOnclickAttribute": None,
|
"mirrorOnclickAttribute": None,
|
||||||
"mirrorOnclickProperty": True,
|
"mirrorOnclickProperty": True,
|
||||||
"mirrorBeforeDashboard": True,
|
"mirrorBeforeDashboard": True,
|
||||||
|
"initialOrder": [
|
||||||
|
"chatMobileBtn",
|
||||||
|
"hwxThemeCreatorRailBtnMobile",
|
||||||
|
"hwxTypographyRailBtnMobile",
|
||||||
|
"dashboardMobileBtn",
|
||||||
|
"settingsMobileBtn",
|
||||||
|
],
|
||||||
|
"preSyncOrder": [
|
||||||
|
"chatMobileBtn",
|
||||||
|
"hwxThemeCreatorRailBtnMobile",
|
||||||
|
"hwxTypographyRailBtnMobile",
|
||||||
|
"tasksMobileBtn",
|
||||||
|
"logsMobileBtn",
|
||||||
|
"dashboardMobileBtn",
|
||||||
|
"settingsMobileBtn",
|
||||||
|
],
|
||||||
|
"reconciledOrder": [
|
||||||
|
"chatMobileBtn",
|
||||||
|
"tasksMobileBtn",
|
||||||
|
"logsMobileBtn",
|
||||||
|
"hwxThemeCreatorRailBtnMobile",
|
||||||
|
"hwxTypographyRailBtnMobile",
|
||||||
|
"dashboardMobileBtn",
|
||||||
|
"settingsMobileBtn",
|
||||||
|
],
|
||||||
|
"reconciliationMoves": 2,
|
||||||
|
"noOpSyncMoves": 0,
|
||||||
"mirrorRemoved": True,
|
"mirrorRemoved": True,
|
||||||
|
"noAnchorMirrorAppended": True,
|
||||||
"clicked": 1,
|
"clicked": 1,
|
||||||
"sidebarClosed": 1,
|
"sidebarClosed": 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,72 @@ def test_capture_snapshot_stores_content_addressed_bytes(snap_dir, tmp_path):
|
|||||||
assert stored.read_bytes() == b"<html>v1</html>"
|
assert stored.read_bytes() == b"<html>v1</html>"
|
||||||
|
|
||||||
|
|
||||||
|
def test_anchored_snapshot_read_preserves_windows_ctrl_z(tmp_path):
|
||||||
|
"""Anchored binary reads must not treat DOS Ctrl-Z as text EOF on Windows."""
|
||||||
|
from api.routes import _etag_and_snapshot, _open_file_read_fd
|
||||||
|
|
||||||
|
payload = b"a" * 568 + b"\x1a" + b"b" * 4096
|
||||||
|
source = tmp_path / "clip.mp4"
|
||||||
|
source.write_bytes(payload)
|
||||||
|
|
||||||
|
fd = _open_file_read_fd(source, tmp_path)
|
||||||
|
try:
|
||||||
|
_etag, snapshot, actual_size = _etag_and_snapshot(fd, file_size=len(payload))
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
assert actual_size == len(payload)
|
||||||
|
assert snapshot == payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_anchored_file_leaf_uses_binary_open_flag(monkeypatch, tmp_path):
|
||||||
|
"""The platform-independent flag contract keeps Windows binary-safe in CI."""
|
||||||
|
from api import workspace
|
||||||
|
|
||||||
|
source = tmp_path / "clip.mp4"
|
||||||
|
source.write_bytes(b"payload")
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
def fake_open(path, flags, *args, **kwargs):
|
||||||
|
seen["path"] = path
|
||||||
|
seen["flags"] = flags
|
||||||
|
return 123
|
||||||
|
|
||||||
|
monkeypatch.setattr(workspace, "_DIR_FD_OK", False)
|
||||||
|
monkeypatch.setattr(workspace, "_O_BINARY", 0x40000000)
|
||||||
|
monkeypatch.setattr(workspace.os, "open", fake_open)
|
||||||
|
|
||||||
|
fd = workspace.open_anchored_fd(tmp_path, source, want_dir=False)
|
||||||
|
|
||||||
|
assert fd == 123
|
||||||
|
assert seen["path"] == str(source)
|
||||||
|
assert seen["flags"] & workspace._O_BINARY
|
||||||
|
|
||||||
|
|
||||||
|
def test_anchored_directory_does_not_use_binary_open_flag(monkeypatch, tmp_path):
|
||||||
|
"""Directory opens keep their directory-only flag contract."""
|
||||||
|
from api import workspace
|
||||||
|
|
||||||
|
target = tmp_path / "folder"
|
||||||
|
target.mkdir()
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
def fake_open(path, flags, *args, **kwargs):
|
||||||
|
seen["flags"] = flags
|
||||||
|
return 124
|
||||||
|
|
||||||
|
monkeypatch.setattr(workspace, "_DIR_FD_OK", False)
|
||||||
|
monkeypatch.setattr(workspace, "_O_BINARY", 0x40000000)
|
||||||
|
monkeypatch.setattr(workspace, "_O_DIRECTORY", 0x20000000)
|
||||||
|
monkeypatch.setattr(workspace.os, "open", fake_open)
|
||||||
|
|
||||||
|
fd = workspace.open_anchored_fd(tmp_path, target, want_dir=True)
|
||||||
|
|
||||||
|
assert fd == 124
|
||||||
|
assert seen["flags"] & workspace._O_DIRECTORY
|
||||||
|
assert not seen["flags"] & workspace._O_BINARY
|
||||||
|
|
||||||
|
|
||||||
def test_capture_snapshot_dedupes_identical_content(snap_dir, tmp_path):
|
def test_capture_snapshot_dedupes_identical_content(snap_dir, tmp_path):
|
||||||
from api.media_snapshots import capture_snapshot
|
from api.media_snapshots import capture_snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -158,12 +158,24 @@ def test_playwright_regression_ensures_font_contract_for_syntax_and_edit_surface
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<button id="buttonSentinel">button</button>
|
||||||
|
<input id="inputSentinel">
|
||||||
|
<select id="selectSentinel"><option>select</option></select>
|
||||||
|
<textarea id="textareaSentinel"></textarea>
|
||||||
<div class="msg-body">
|
<div class="msg-body">
|
||||||
<p>inline <code id="inlineCode" class="language-js">token</code> sample.</p>
|
<p>inline <code id="inlineCode" class="language-js">token</code> sample.</p>
|
||||||
<pre id="fencedPre" class="language-js"><code id="fencedCode" class="language-js">fenced token</code></pre>
|
<pre id="fencedPre" class="language-js"><code id="fencedCode" class="language-js">fenced token</code></pre>
|
||||||
|
<input class="markdown-table-filter" id="markdownTableFilter">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th><button class="markdown-table-sort" id="markdownTableSort">sort</button></th></tr></thead>
|
||||||
|
<tbody><tr><td id="markdownTableCell">row</td></tr></tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<textarea class="msg-edit-area" id="msgEditArea"></textarea>
|
<textarea class="msg-edit-area" id="msgEditArea"></textarea>
|
||||||
<textarea id="previewEditArea" style="{preview_style_match.group(1)}"></textarea>
|
<textarea id="previewEditArea" style="{preview_style_match.group(1)}"></textarea>
|
||||||
|
<div class="detail-form-row">
|
||||||
|
<textarea id="detailFormTextarea"></textarea>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -206,6 +218,22 @@ def test_playwright_regression_ensures_font_contract_for_syntax_and_edit_surface
|
|||||||
"#previewEditArea",
|
"#previewEditArea",
|
||||||
"el => getComputedStyle(el).fontFamily",
|
"el => getComputedStyle(el).fontFamily",
|
||||||
)
|
)
|
||||||
|
component_fonts = page.evaluate(
|
||||||
|
"""
|
||||||
|
() => Object.fromEntries(
|
||||||
|
["markdownTableFilter", "markdownTableSort", "markdownTableCell", "detailFormTextarea"]
|
||||||
|
.map(id => [id, getComputedStyle(document.getElementById(id)).fontFamily])
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
native_control_fonts = page.evaluate(
|
||||||
|
"""
|
||||||
|
() => Object.fromEntries(
|
||||||
|
["buttonSentinel", "inputSentinel", "selectSentinel", "textareaSentinel"]
|
||||||
|
.map(id => [id, getComputedStyle(document.getElementById(id)).fontFamily])
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
runtime_token_font = page.evaluate("""
|
runtime_token_font = page.evaluate("""
|
||||||
() => {
|
() => {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
@@ -248,6 +276,13 @@ def test_playwright_regression_ensures_font_contract_for_syntax_and_edit_surface
|
|||||||
assert "MonoFontSentinel" in fenced_code_font
|
assert "MonoFontSentinel" in fenced_code_font
|
||||||
assert "ConvoFontSentinel" in message_edit_font
|
assert "ConvoFontSentinel" in message_edit_font
|
||||||
assert "MonoFontSentinel" in preview_edit_font
|
assert "MonoFontSentinel" in preview_edit_font
|
||||||
|
for control_id, font in native_control_fonts.items():
|
||||||
|
assert "UiFontSentinel" in font, f"{control_id} computed {font!r}"
|
||||||
|
assert "UiFontSentinel" in component_fonts["markdownTableFilter"]
|
||||||
|
assert "MonoFontSentinel" in component_fonts["markdownTableSort"]
|
||||||
|
assert "MonoFontSentinel" in component_fonts["markdownTableCell"]
|
||||||
|
assert component_fonts["markdownTableSort"] == component_fonts["markdownTableCell"]
|
||||||
|
assert "MonoFontSentinel" in component_fonts["detailFormTextarea"]
|
||||||
assert "RuntimeMono" in runtime_token_font
|
assert "RuntimeMono" in runtime_token_font
|
||||||
assert "EditedRuntimeMono" in edited_runtime_token_font
|
assert "EditedRuntimeMono" in edited_runtime_token_font
|
||||||
assert "LinkedRuntimeMono" in linked_stylesheet_font
|
assert "LinkedRuntimeMono" in linked_stylesheet_font
|
||||||
|
|||||||
Reference in new issue
Block a user