Merge pull request #558 from carlulsoe/mobile-ui-improvements
feat(ui): improve mobile responsiveness [AI-assisted]
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- macOS: add node bridge heartbeat pings to detect half-open sockets and reconnect cleanly. (#572) — thanks @ngutman
|
- macOS: add node bridge heartbeat pings to detect half-open sockets and reconnect cleanly. (#572) — thanks @ngutman
|
||||||
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
||||||
|
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
||||||
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
||||||
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
||||||
- Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro
|
- Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro
|
||||||
|
|||||||
BIN
docs/images/mobile-ui-screenshot.png
Normal file
BIN
docs/images/mobile-ui-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
@@ -56,7 +56,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|||||||
const previousUserProfile = process.env.USERPROFILE;
|
const previousUserProfile = process.env.USERPROFILE;
|
||||||
const previousHomeDrive = process.env.HOMEDRIVE;
|
const previousHomeDrive = process.env.HOMEDRIVE;
|
||||||
const previousHomePath = process.env.HOMEPATH;
|
const previousHomePath = process.env.HOMEPATH;
|
||||||
|
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||||
|
const previousClawdisStateDir = process.env.CLAWDIS_STATE_DIR;
|
||||||
process.env.HOME = base;
|
process.env.HOME = base;
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = join(base, ".clawdbot");
|
||||||
|
process.env.CLAWDIS_STATE_DIR = join(base, ".clawdbot");
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
process.env.USERPROFILE = base;
|
process.env.USERPROFILE = base;
|
||||||
const driveMatch = base.match(/^([A-Za-z]:)(.*)$/);
|
const driveMatch = base.match(/^([A-Za-z]:)(.*)$/);
|
||||||
@@ -74,6 +78,8 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|||||||
process.env.USERPROFILE = previousUserProfile;
|
process.env.USERPROFILE = previousUserProfile;
|
||||||
process.env.HOMEDRIVE = previousHomeDrive;
|
process.env.HOMEDRIVE = previousHomeDrive;
|
||||||
process.env.HOMEPATH = previousHomePath;
|
process.env.HOMEPATH = previousHomePath;
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = previousStateDir;
|
||||||
|
process.env.CLAWDIS_STATE_DIR = previousClawdisStateDir;
|
||||||
await fs.rm(base, { recursive: true, force: true });
|
await fs.rm(base, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ function sendLine(socket: net.Socket, obj: unknown) {
|
|||||||
socket.write(`${JSON.stringify(obj)}\n`);
|
socket.write(`${JSON.stringify(obj)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForSocketConnect(socket: net.Socket) {
|
||||||
|
if (!socket.connecting) return;
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
socket.once("connect", resolve);
|
||||||
|
socket.once("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("node bridge server", () => {
|
describe("node bridge server", () => {
|
||||||
let baseDir = "";
|
let baseDir = "";
|
||||||
|
|
||||||
@@ -156,6 +164,7 @@ describe("node bridge server", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const socket = net.connect({ host: "127.0.0.1", port: server.port });
|
const socket = net.connect({ host: "127.0.0.1", port: server.port });
|
||||||
|
await waitForSocketConnect(socket);
|
||||||
const readLine = createLineReader(socket);
|
const readLine = createLineReader(socket);
|
||||||
sendLine(socket, { type: "pair-request", nodeId: "n2", platform: "ios" });
|
sendLine(socket, { type: "pair-request", nodeId: "n2", platform: "ios" });
|
||||||
|
|
||||||
@@ -189,6 +198,7 @@ describe("node bridge server", () => {
|
|||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
|
||||||
const socket2 = net.connect({ host: "127.0.0.1", port: server.port });
|
const socket2 = net.connect({ host: "127.0.0.1", port: server.port });
|
||||||
|
await waitForSocketConnect(socket2);
|
||||||
const readLine2 = createLineReader(socket2);
|
const readLine2 = createLineReader(socket2);
|
||||||
sendLine(socket2, { type: "hello", nodeId: "n2", token });
|
sendLine(socket2, { type: "hello", nodeId: "n2", token });
|
||||||
const line3 = JSON.parse(await readLine2()) as { type: string };
|
const line3 = JSON.parse(await readLine2()) as { type: string };
|
||||||
@@ -239,6 +249,7 @@ describe("node bridge server", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const socket = net.connect({ host: "127.0.0.1", port: server.port });
|
const socket = net.connect({ host: "127.0.0.1", port: server.port });
|
||||||
|
await waitForSocketConnect(socket);
|
||||||
const readLine = createLineReader(socket);
|
const readLine = createLineReader(socket);
|
||||||
sendLine(socket, {
|
sendLine(socket, {
|
||||||
type: "pair-request",
|
type: "pair-request",
|
||||||
@@ -248,7 +259,7 @@ describe("node bridge server", () => {
|
|||||||
|
|
||||||
// Approve the pending request from the gateway side.
|
// Approve the pending request from the gateway side.
|
||||||
let reqId: string | undefined;
|
let reqId: string | undefined;
|
||||||
for (let i = 0; i < 40; i += 1) {
|
for (let i = 0; i < 120; i += 1) {
|
||||||
const list = await listNodePairing(baseDir);
|
const list = await listNodePairing(baseDir);
|
||||||
const req = list.pending.find((p) => p.nodeId === "n3-rpc");
|
const req = list.pending.find((p) => p.nodeId === "n3-rpc");
|
||||||
if (req) {
|
if (req) {
|
||||||
|
|||||||
@@ -363,3 +363,277 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile-specific improvements */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.shell {
|
||||||
|
--shell-pad: 8px;
|
||||||
|
--shell-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact topbar for mobile */
|
||||||
|
.topbar {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-sub {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-status {
|
||||||
|
gap: 6px;
|
||||||
|
width: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-status .pill {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-status .pill .mono {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-status .pill span:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal scrollable nav for mobile */
|
||||||
|
.nav {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide page title on mobile - nav already shows where you are */
|
||||||
|
.content-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 4px 4px 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smaller cards on mobile */
|
||||||
|
.card {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat grid adjustments */
|
||||||
|
.stat-grid {
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notes grid */
|
||||||
|
.note-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form fields */
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input,
|
||||||
|
.field textarea,
|
||||||
|
.field select {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pills */
|
||||||
|
.pill {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat-specific mobile improvements */
|
||||||
|
.chat-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header__left {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header__right {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-session {
|
||||||
|
min-width: unset;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thread {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg {
|
||||||
|
max-width: 92%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-compose {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-compose__field textarea {
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log stream mobile */
|
||||||
|
.log-stream {
|
||||||
|
border-radius: 10px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-subsystem {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide docs link on mobile - saves space */
|
||||||
|
.docs-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List items */
|
||||||
|
.list-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
.code-block {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme toggle smaller */
|
||||||
|
.theme-toggle {
|
||||||
|
--theme-item: 24px;
|
||||||
|
--theme-gap: 4px;
|
||||||
|
--theme-pad: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user