diff --git a/.gitignore b/.gitignore
index cbebb7e9..69c8d805 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,4 +39,7 @@ out/
# AI
skills-lock.json
.agents
-.claude
\ No newline at end of file
+.claude
+
+# Projects
+projects/
\ No newline at end of file
diff --git a/apps/cli/src/cli-client.ts b/apps/cli/src/cli-client.ts
index fc125a02..b1b04037 100644
--- a/apps/cli/src/cli-client.ts
+++ b/apps/cli/src/cli-client.ts
@@ -36,14 +36,22 @@ function requestConnection(handshake: CliHandshake, timeoutMs: number): Promise<
sock.setTimeout(timeoutMs, () =>
settle(() => reject(new Error("Timed out waiting for the app to accept the connection"))),
);
- sock.on("connect", () => sock.end(JSON.stringify(handshake)));
+ sock.on("connect", () => sock.write(JSON.stringify(handshake) + "\n"));
sock.on("data", (chunk) => {
buf += chunk;
+ try {
+ const reply = JSON.parse(buf.trim()) as CliHandshakeReply;
+ if (reply.ok) settle(resolve);
+ else settle(() => reject(new Error(reply.error)));
+ } catch {
+ // waiting for full json
+ }
});
sock.on("end", () => {
+ if (settled) return;
let reply: CliHandshakeReply;
try {
- reply = JSON.parse(buf) as CliHandshakeReply;
+ reply = JSON.parse(buf.trim()) as CliHandshakeReply;
} catch (e) {
settle(() => reject(e instanceof Error ? e : new Error(String(e))));
return;
diff --git a/apps/desktop/src/cli-server.ts b/apps/desktop/src/cli-server.ts
index c78cf301..b1174cae 100644
--- a/apps/desktop/src/cli-server.ts
+++ b/apps/desktop/src/cli-server.ts
@@ -107,24 +107,46 @@ export function startCliServer() {
cliServer = createServer({ allowHalfOpen: true }, (sock: Socket) => {
enableHeadless();
let buf = "";
+ let handled = false;
sock.setEncoding("utf8");
sock.setTimeout(60000, () => sock.destroy());
- sock.on("data", (chunk) => {
- buf += chunk;
- });
- sock.on("end", async () => {
- sock.setTimeout(0);
+
+ const tryProcess = async () => {
+ if (handled) return;
let handshake: CliHandshake;
try {
- handshake = JSON.parse(buf) as CliHandshake;
+ handshake = JSON.parse(buf.trim()) as CliHandshake;
if (typeof handshake.port !== "number" || typeof handshake.token !== "string") {
- throw new Error("Malformed handshake");
+ return;
}
} catch {
- sock.end(JSON.stringify({ ok: false, error: "Invalid handshake" }));
return;
}
+ handled = true;
+ sock.setTimeout(0);
await deliverHandshake(handshake, sock);
+ };
+
+ sock.on("data", async (chunk) => {
+ buf += chunk;
+ await tryProcess();
+ });
+ sock.on("end", async () => {
+ if (!handled) {
+ sock.setTimeout(0);
+ let handshake: CliHandshake;
+ try {
+ handshake = JSON.parse(buf.trim()) as CliHandshake;
+ if (typeof handshake.port !== "number" || typeof handshake.token !== "string") {
+ throw new Error("Malformed handshake");
+ }
+ } catch {
+ if (!sock.destroyed) sock.end(JSON.stringify({ ok: false, error: "Invalid handshake" }));
+ return;
+ }
+ handled = true;
+ await deliverHandshake(handshake, sock);
+ }
});
sock.on("error", () => {
// Client hung up; nothing to do.
diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx
index bb490a29..c3f68f79 100644
--- a/apps/web/src/app.tsx
+++ b/apps/web/src/app.tsx
@@ -35,7 +35,7 @@ function AuthGate(props: { children: JSX.Element }) {
{props.children}
-
+
diff --git a/apps/web/src/components/sidebar-right/inspector/animations.tsx b/apps/web/src/components/sidebar-right/inspector/animations.tsx
index 59380100..6715fda3 100644
--- a/apps/web/src/components/sidebar-right/inspector/animations.tsx
+++ b/apps/web/src/components/sidebar-right/inspector/animations.tsx
@@ -45,10 +45,16 @@ import { ANIMATION_GROUPS, DEFAULT_ANIMATION, animationOption } from "./animatio
import type { AnimationGroup, AnimationOption } from "./animation-types";
import type { Entity } from "koota";
-/** ``'s defaults; a control left at one of these unsets its prop. */
const DEFAULT_DURATION = 1;
const DEFAULT_DELAY = 0;
+type PhaseOption = { value: "in" | "out"; label: string };
+
+const PHASE_OPTIONS: PhaseOption[] = [
+ { value: "in", label: "In" },
+ { value: "out", label: "Out" },
+];
+
// Stable identity, so a node without animations does not resample every tick.
const NO_ANIMATIONS: Entity[] = [];
@@ -211,6 +217,7 @@ function AnimationInspector(props: AnimationInspectorProps) {
const duration = createMemo(() => framesToSeconds(animation()?.duration ?? 0, fps()));
const delay = createMemo(() => framesToSeconds(animation()?.delay ?? 0, fps()));
const isOut = createMemo(() => animation()?.phase === AnimationPhase.OUT);
+ const selectedPhase = createMemo(() => (isOut() ? PHASE_OPTIONS[1]! : PHASE_OPTIONS[0]!));
/**
* The groups this node can play, plus whichever one holds the current
@@ -287,18 +294,22 @@ function AnimationInspector(props: AnimationInspectorProps) {
-