Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ jobs:
node scripts/home-shell-regression-smoke.mjs
node scripts/home-agent-shell-smoke.mjs
node --test scripts/home-agent-cancellation.test.mjs
node --test scripts/home-link-status.test.mjs
node --test scripts/chat-room-enter.test.mjs
node scripts/people-discovery-smoke.mjs
- name: release artifact source proofs
run: |
Expand Down
2 changes: 1 addition & 1 deletion capsules/_shared/model-management.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@
progress.setAttribute("aria-label", "Preparation progress"); row.append(progress, element("p", `${formatBytes(p.completed_bytes)} of ${formatBytes(p.total_bytes)} prepared`));
}
const controls = element("div", "", "model-controls");
const use = button((p && p.state !== "reclaimed") || unresolvedUse ? "Retry" : compact ? "Get" : "Use", () => void act("use"), active(p) || reconcileRequired);
const use = button((p && p.state !== "reclaimed") || unresolvedUse ? "Retry" : compact ? "Get" : "Download", () => void act("use"), active(p) || reconcileRequired);
use.dataset.modelControl = "use";
if (!active(p) && !r.dispatch_ready) controls.append(use);
if (r.dispatch_ready && typeof onReadyOpen === "function") {
Expand Down
13 changes: 10 additions & 3 deletions capsules/assistant/browser/agent-harness.css
Original file line number Diff line number Diff line change
Expand Up @@ -1456,13 +1456,13 @@ body.agent-harness-sidebar-collapsed #agent-harness-search-open-main {
grid-template-columns: minmax(7rem, 9rem) minmax(0, 1fr);
gap: 6px 12px;
margin: 12px 0 0;
color: color-mix(in srgb, var(--text) 78%, transparent);
color: color-mix(in srgb, var(--text) 82%, transparent);
font-size: 12px;
}

.agent-model-selection-facts dt {
margin: 0;
font-weight: 650;
color: color-mix(in srgb, var(--text) 48%, transparent);
}

.agent-model-selection-facts dd {
Expand Down Expand Up @@ -3095,6 +3095,12 @@ html[data-agent-reasoning="off"] .agent-thinking {
height: 32px;
}

/* When the row is tight the model name ellipsizes; the left tools keep their
size. */
.agent-composer-tools-left {
flex-shrink: 0;
}

.agent-composer-attachments {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -3561,11 +3567,12 @@ html[data-agent-reasoning="off"] .agent-thinking {
min-width: 0;
font-size: 13px;
line-height: 1;
white-space: nowrap;
}

.agent-model-name {
display: inline-block;
min-width: 0;
min-width: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
Expand Down
33 changes: 33 additions & 0 deletions capsules/chat-room/browser/chat-chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,39 @@
messageInput.addEventListener("input", syncComposerPresentation);
}

// Until the shell names the form factor, the device screen stands in for
// it: this window's own width is the app window, not the device. Same
// 640 px phone class as the Home shell.
function isPhone() {
const named = document.documentElement.dataset.elFormFactor;
if (named) {
return named === "phone";
}
const screen = window.screen || {};
const coarse = Boolean(window.matchMedia?.("(pointer: coarse)")?.matches
|| window.matchMedia?.("(hover: none)")?.matches);
return screen.width <= 640 || (coarse && screen.height <= 640);
}

if (messageInput && composerForm) {
messageInput.addEventListener("keydown", (event) => {
// 229: Safari reports an IME's confirming Enter without isComposing.
if (event.key !== "Enter" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey
|| event.isComposing || event.keyCode === 229) {
return;
}
// A phone keyboard's Return adds a line; Send sends.
if (isPhone()) {
return;
}
event.preventDefault();
const sendButton = document.getElementById("send-button");
if (sendButton instanceof HTMLButtonElement && !sendButton.disabled) {
composerForm.requestSubmit(sendButton);
}
});
}

if (roomAccessToggle) {
new MutationObserver(() => {
announceHomeChrome();
Expand Down
5 changes: 5 additions & 0 deletions capsules/home-gui/browser/home-gui-shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isTrustedHomeGuiMessage,
projectHomeGuiAuthority,
} from "./home-gui-authority.js?v=home-20260813a";
import { applyHomeLinkStatus } from "./shell-link-status.js?v=home-20260813a";

const route = new URL(window.location.href);
const fragment = new URLSearchParams(route.hash.replace(/^#/, ""));
Expand Down Expand Up @@ -368,6 +369,10 @@ window.addEventListener("message", (event) => {
settleRequest(message);
return;
}
if (message.type === "home:link-status") {
applyHomeLinkStatus(message);
return;
}
if (message.type === "home:shell-summary") {
applySummary(message.summary).catch((error) => {
document.body.dataset.homeStatus = "error";
Expand Down
8 changes: 8 additions & 0 deletions capsules/home-gui/browser/home-gui-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
<span id="toolbar-active-title" class="toolbar-active-title">Home</span>
<nav id="toolbar-menubar" class="toolbar-menubar" aria-label="App menus"></nav>
<div class="toolbar-spacer"></div>
<span id="toolbar-link-status" class="toolbar-link-status" role="status">
<span class="toolbar-link-status-dot" aria-hidden="true"></span>
<span id="toolbar-link-status-text" class="toolbar-link-status-text"></span>
</span>
<button
id="toolbar-wallet"
class="toolbar-btn wallet-btn"
Expand Down Expand Up @@ -145,6 +149,10 @@
inert
hidden
>
<div class="notification-center-link">
<p class="notification-center-link-title">Reconnecting to your Home…</p>
<p class="notification-center-link-detail">What you see may be out of date until it answers.</p>
</div>
<div class="nc-cal" aria-label="Date">
<div class="nc-cal-primary">
<div id="nc-cal-date" class="nc-cal-date">—</div>
Expand Down
23 changes: 23 additions & 0 deletions capsules/home-gui/browser/shell-link-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* Home link status, shell side. The host says when this Home's gateway stops
answering (`home:link-status`, home/browser/home-link-status.js). The bar
shows a quiet "Reconnecting…" and Notification Centre explains it, both
keyed on body[data-home-link]. Presentation only. docs/HOME_MOBILE.md. */

export const HOME_LINK_RECONNECTING = "reconnecting";
const BAR_STATUS_TEXT = "Reconnecting…";

// Anything but a boolean `reachable` is ignored.
export function applyHomeLinkStatus(message, doc = document) {
if (typeof message?.reachable !== "boolean") {
return;
}
if (message.reachable) {
delete doc.body.dataset.homeLink;
} else {
doc.body.dataset.homeLink = HOME_LINK_RECONNECTING;
}
const barStatus = doc.querySelector("#toolbar-link-status-text");
if (barStatus) {
barStatus.textContent = message.reachable ? "" : BAR_STATUS_TEXT;
}
}
39 changes: 17 additions & 22 deletions capsules/home-gui/browser/shell-windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
saveShellSessionState,
ignoreRepeatedAction,
targetById,
WINDOW_TOP_INSET,
WINDOW_BOTTOM_INSET,
} from "./shell-core.js?v=home-20260813a";
import {
fitWindowBounds,
Expand Down Expand Up @@ -2101,34 +2103,27 @@ function browserWindowSpec(launched, offset) {
return walletConnectorWindowSpec();
}
if (launched.target === SYSTEM_APP_ID) {
return {
x: 36,
y: 44,
width: 980,
height: 620,
};
return centredWindowSpec({ width: 980, height: 620 }, 0, 0);
}
if (launched.target === "gba-emulator") {
return {
x: 88 + offset * 24,
y: 62 + offset * 20,
width: 900,
height: 620,
};
return centredWindowSpec({ width: 900, height: 620 }, offset * 24, offset * 20);
}
if (launched.target === "browser") {
return {
x: 48 + offset * 18,
y: 54 + offset * 18,
width: 1280,
height: 804,
};
return centredWindowSpec({ width: 1280, height: 804 }, offset * 18, offset * 18);
}
return centredWindowSpec({ width: 1040, height: 720 }, offset * 26, offset * 22);
}

function centredWindowSpec({ width, height }, cascadeX, cascadeY) {
const fitted = fitWindowBounds({ x: 0, y: 0, width, height });
const workspaceRect = desktop.getBoundingClientRect();
const roomWidth = window.innerWidth - workspaceRect.left;
const roomHeight = window.innerHeight - workspaceRect.top - WINDOW_TOP_INSET - WINDOW_BOTTOM_INSET;
return {
x: 104 + offset * 26,
y: 78 + offset * 22,
width: 1040,
height: 720,
x: Math.round((roomWidth - fitted.width) / 2) + cascadeX,
y: WINDOW_TOP_INSET + Math.round((roomHeight - fitted.height) / 2) + cascadeY,
width,
height,
};
}

Expand Down
76 changes: 76 additions & 0 deletions capsules/home-gui/browser/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,82 @@ img {
font-weight: 600;
}

/* Home link status (shell-link-status.js): while the host reports that this
Home's gateway is not answering, the bar shows a pulsing dot and
"Reconnecting…", and Notification Centre opens with the explanation. The
bar status stays rendered (empty) so its live region is announced. */
.toolbar-link-status {
display: inline-flex;
flex: 0 1 auto;
align-items: center;
gap: 6px;
min-width: 0;
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}

.toolbar-link-status-text {
overflow: hidden;
text-overflow: ellipsis;
}

.toolbar-link-status-dot,
.notification-center-link {
display: none;
}

.toolbar-link-status-dot {
flex: 0 0 auto;
width: 8px;
height: 8px;
border-radius: 50%;
background: #f5a524;
animation: toolbar-link-status-pulse 1.6s ease-in-out infinite;
}

@keyframes toolbar-link-status-pulse {
50% {
opacity: 0.35;
}
}

@media (prefers-reduced-motion: reduce) {
.toolbar-link-status-dot {
animation: none;
}
}

body[data-home-link="reconnecting"] .toolbar-link-status {
margin-right: 8px;
}

body[data-home-link="reconnecting"] .toolbar-link-status-dot,
body[data-home-link="reconnecting"] .notification-center-link {
display: block;
}

.notification-center-link {
flex: 0 0 auto;
margin: 0 0 8px;
padding: 10px 12px;
border-radius: 10px;
background: var(--lift);
}

.notification-center-link-title {
margin: 0;
color: var(--text);
font-size: 13px;
font-weight: 600;
}

.notification-center-link-detail {
margin: 2px 0 0;
color: var(--muted);
font-size: 12px;
}

.notification-center-clear {
padding: 3px 8px;
border: 0;
Expand Down
81 changes: 81 additions & 0 deletions capsules/home/browser/home-link-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* Home link status. The host page is the one document that talks to this
Home's gateway. When the gateway stops answering (a phone leaving Wi-Fi,
the gateway restarting) the host tells the active shell
(`home:link-status`), so the shell says "Reconnecting" instead of quietly
showing stale state. While the link is down the host retries on a short
timer; the first answer clears it. Presentation only — no authority.
See docs/HOME_MOBILE.md. */

export const LINK_STATUS_MESSAGE = "home:link-status";
export const LINK_RETRY_MS = 3_000;
// A proxy in front of the gateway answering for it while it is gone.
const GATEWAY_GONE_STATUSES = new Set([502, 503, 504]);

// fetch rejects with a TypeError in every engine when the request never
// reaches the gateway; an HTTP answer carries `status` (shell-core fetchJson).
export function isGatewayUnreachable(error) {
if (error instanceof TypeError) {
return true;
}
return GATEWAY_GONE_STATUSES.has(Number(error?.status));
}

// `post` delivers to the active shell; `retry` asks the gateway again (the
// host's summary refresh, whose answer is what the shell needs anyway).
export function createHomeLinkStatus({
post,
retry,
retryMs = LINK_RETRY_MS,
setTimer = (callback, ms) => window.setTimeout(callback, ms),
clearTimer = (timer) => window.clearTimeout(timer),
}) {
let reachable = true;
let retryTimer = null;

const announce = () => post({ type: LINK_STATUS_MESSAGE, reachable });
const cancelRetry = () => {
if (retryTimer !== null) {
clearTimer(retryTimer);
retryTimer = null;
}
};
const markReachable = () => {
cancelRetry();
if (!reachable) {
reachable = true;
announce();
}
};

return {
reachable: () => reachable,
// True when the failure means the gateway is unreachable (the caller has
// nothing more to report); false for any answer the gateway gave.
reportFailure(error) {
if (!isGatewayUnreachable(error)) {
// Any other HTTP status is an answer, so the link is back.
if (Number.isInteger(error?.status)) {
markReachable();
}
return false;
}
if (reachable) {
reachable = false;
announce();
}
cancelRetry();
retryTimer = setTimer(() => {
retryTimer = null;
retry();
}, retryMs);
return true;
},
reportSuccess: markReachable,
// A reloaded shell starts out reachable; tell it again while down.
replay() {
if (!reachable) {
announce();
}
},
};
}
Loading
Loading