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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function Channel
created: 2026-10-22T21:36:22Z
norm: '1'
sig: eaede8e100f39ef4
body_hash: d29a81f75aaca2c5
raw_hash: 24baf292b9594958
body_hash: d4eb25af070d2db1
raw_hash: 41a9c1eb90325b28
vouched: 2026-10-27T15:34:57Z
lines: 305-439
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function useLayout
created: 2026-10-22T21:09:48Z
norm: '1'
sig: 6f69f0bcaa0b8bb2
body_hash: 25edc7f0a5cbd93a
raw_hash: 01b3bdc9e40f5226
body_hash: 88964a1f7f5adf12
raw_hash: c41ee0b85dab63b9
vouched: 2026-10-27T15:34:58Z
lines: 755-999
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5YZZPJW4GZEWHECPRQ0KG88
anchor: function Live
created: 2026-10-27T15:34:41Z
norm: '1'
sig: acb4d23736381074
body_hash: 4a5367bec438cf3d
raw_hash: 713b5a7ee3ac4295
lines: 13-83
---

status is polled on a timer too; animation frames pause while Ace is covered
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
id: 01M5YZZPFECK4WJBTAX87MNMYE
anchor: function ace_live_show
created: 2026-10-27T15:34:41Z
norm: '1'
---

the live view must stay the content view's top subview; Electrobun re-stacks the webview above it on resize
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5YZZPH2ADYVC2F2ACMCP7B2
anchor: '@file'
created: 2026-10-27T15:34:41Z
norm: '1'
sig: f5f542a9730901ae
body_hash: 0d3d7801d2eeec11
raw_hash: b7c0ca684f0d3c15
lines: 1-249
---

a closed window's SCStream stops without calling didStopWithError, so existence is polled
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function controls
created: 2026-10-15T03:41:11Z
norm: '1'
sig: 93cc5b6836667bee
body_hash: 858640fcc9e00b29
raw_hash: bc3a3bbeda94ba1f
vouched: 2026-10-15T14:10:43Z
body_hash: 4ebf778076e86026
raw_hash: 192bdb24241a0f60
vouched: 2026-10-27T15:34:58Z
lines: 39-86
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-15T04:08:49Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 177d0cc7c38c0a16
raw_hash: 587e4316030f6250
vouched: 2026-10-23T12:27:32Z
body_hash: aa81d073e484e685
raw_hash: 9056b5baf04473f5
vouched: 2026-10-27T15:34:58Z
lines: 266-519
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-15T06:00:20Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 177d0cc7c38c0a16
raw_hash: 587e4316030f6250
vouched: 2026-10-23T12:27:32Z
body_hash: aa81d073e484e685
raw_hash: 9056b5baf04473f5
vouched: 2026-10-27T15:34:58Z
lines: 281-561
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-19T21:09:57Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 177d0cc7c38c0a16
raw_hash: 587e4316030f6250
vouched: 2026-10-23T12:27:33Z
body_hash: aa81d073e484e685
raw_hash: 9056b5baf04473f5
vouched: 2026-10-27T15:34:58Z
lines: 384-700
supersedes: 01M5APK9YCQH467YNMKAC8BT5B
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-20T17:04:56Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: fe20eb5454fb7a15
raw_hash: f105febd5bb519a1
vouched: 2026-10-23T15:02:23Z
body_hash: 1c4d9341aafad755
raw_hash: e1a141ae7ad90111
vouched: 2026-10-27T15:34:58Z
lines: 1-1153
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-22T14:00:47Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: fe20eb5454fb7a15
raw_hash: f105febd5bb519a1
vouched: 2026-10-23T15:02:24Z
body_hash: 1c4d9341aafad755
raw_hash: e1a141ae7ad90111
vouched: 2026-10-27T15:34:58Z
lines: 1-1309
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function tabs
created: 2026-10-15T03:39:44Z
norm: '1'
sig: dfcd4ce2368aa381
body_hash: 2b698f7e26a845da
raw_hash: 8685549985734ece
body_hash: b83e13913bd23af1
raw_hash: 0c3f1f5bff043e6f
vouched: 2026-10-27T15:34:58Z
lines: 15-49
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function tabs
created: 2026-10-15T03:39:45Z
norm: '1'
sig: dfcd4ce2368aa381
body_hash: 2b698f7e26a845da
raw_hash: 8685549985734ece
body_hash: b83e13913bd23af1
raw_hash: 0c3f1f5bff043e6f
vouched: 2026-10-27T15:34:58Z
lines: 15-49
---

Expand Down
12 changes: 12 additions & 0 deletions apps/app/src/channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { useAsked, useGit } from "./git";
import { host, onOpen as onHostOpen } from "./host";
import { CHAT, Layout } from "./layout/layout";
import { Diff, GROUPS } from "./panels/diff";
import { Live } from "./panels/live";
import { Terminal } from "./panels/terminal";
import { placement } from "./placement";
import type { Item } from "./transcript";
Expand Down Expand Up @@ -409,6 +410,17 @@ export function Channel(
if (data.type === "diff") {
return <Diff channel={channel} chat={data.chat} user={user} active={active} />;
}
if (data.type === "live") {
return (
<Live
id={uid}
window={data.window}
app={data.app}
active={active}
update={update}
/>
);
}
if (data.type !== "terminal") return null;
// Tabs saved before Terminal was withheld here still open, so they say why they're empty.
if (elsewhere) {
Expand Down
15 changes: 15 additions & 0 deletions apps/app/src/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ export const desktop = view && token
project: () => rpc.request.project({ token }),
lights: (expanded: boolean) => rpc.request.lights({ token, expanded }),
zoom: () => rpc.request.zoom({ token }),
live: (
id: string,
window: number | null,
rect?: DOMRectReadOnly,
relink?: { app?: string },
) =>
rpc.request.live({
token,
id,
window,
...(rect
? { rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } }
: {}),
...(relink ? { relink } : {}),
}),
window: () => rpc.request.window({ token }),
helper: (action: HelperAction) => rpc.request.helper({ token, action }),
native: (action: NativeAction) => rpc.request.native({ token, action }),
Expand Down
39 changes: 39 additions & 0 deletions apps/app/src/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
IconChevronDown,
IconFileDiff,
IconMessage,
IconMonitor,
IconPencil,
IconPlus,
IconRows,
Expand Down Expand Up @@ -122,12 +123,14 @@ const ICONS: Record<Kind, Icon> = {
chat: IconMessage,
diff: IconFileDiff,
terminal: IconTerminal,
live: IconMonitor,
};
const TITLES: Record<Kind, string> = {
blank: "Blank",
chat: "Chat",
diff: "Diff",
terminal: "Terminal",
live: "Live",
};

function minimum(tab?: Split.Tab) {
Expand Down Expand Up @@ -814,6 +817,11 @@ function useLayout(
const command = useEffectEvent((request: TabRename) => {
if (request.expires <= Date.now()) throw new Error("Tab rename expired");
if (request.channel !== id) throw new Error("This window is showing another channel");
if (request.live !== undefined) {
flushSync(() => watch(request.live!, request.name));
persist();
return snapshot();
}
if (!api.get().tabs.includes(request.tab)) throw new Error("Tab is no longer open");
if (request.tab === CHAT) throw new Error("Rename the channel to change its Chat tab");
// The host must acknowledge the name that the client has actually rendered and saved.
Expand Down Expand Up @@ -949,6 +957,37 @@ function useLayout(
requestAnimationFrame(() => focus(uid));
}

// One tab per watched window, split to the right of the chat the first time.
function watch(target: number, name: string) {
// A relaunched app has a new window ID; its tab of the same name follows it.
const open = api.get().tabs.find((uid) => {
const value = meta[uid];
return value?.type === "live" && (value.window === target || !!name && value.name === name);
});
if (open) {
setMeta((meta) => ({
...meta,
[open]: { type: "live", window: target, ...(name ? { name } : undefined) },
}));
return activate(open);
}
const uid = `tab-${api.get().seq + 1}`;
const pane = Split.host(api.get(), CHAT)?.uid;
const side = !flat && Split.can(api.get(), { ...fit, open: { to: { pane, side: "right" } } });
const to: Split.Target | undefined = side
? { pane, side: "right" }
: flat
? undefined
: { pane };
if (!api.open({ tab: uid, to, ...(side ? { fit } : {}) })) {
throw new Error("This Ace window has no room for another tab");
}
setMeta((meta) => ({
...meta,
[uid]: { type: "live", window: target, ...(name ? { name } : undefined) },
}));
}

function swap(uid: string, choice: Choice) {
if (choice === "chat") {
close(uid);
Expand Down
17 changes: 16 additions & 1 deletion apps/app/src/layout/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const CHAT = "chat";

const STORAGE = "ace:channel-layout";
const VERSION = 1;
const KINDS = new Set<Kind>(["blank", "chat", "diff", "terminal"]);
const KINDS = new Set<Kind>(["blank", "chat", "diff", "terminal", "live"]);

/** Diff and Terminal tabs belong to the chat whose lane they show. */
export type Content =
Expand All @@ -16,6 +16,9 @@ export type Content =
| { type: "chat" }
| { type: "diff"; chat: number }
| { type: "terminal"; chat: number; terminal?: string }
// A native window on this Mac, drawn live over the pane; `window` is its CGWindowID and
// `app` the bundle ID that a relink looks for.
| { type: "live"; window: number; app?: string }
)
& { name?: string };

Expand All @@ -33,6 +36,7 @@ function object(value: unknown): value is Record<string, unknown> {

export function content(kind: Kind, chat: number): Content {
if (kind === "diff" || kind === "terminal") return { type: kind, chat };
if (kind === "live") return { type: "blank" };
return { type: kind };
}

Expand All @@ -49,6 +53,17 @@ function clean(state: Split.State, meta?: unknown): Meta {
next[uid] = fallback(uid);
continue;
}
if (value.type === "live") {
next[uid] = Number.isInteger(value.window)
? {
type: "live",
window: value.window as number,
...(typeof value.app === "string" ? { app: value.app } : undefined),
...(typeof value.name === "string" ? { name: value.name } : undefined),
}
: fallback(uid);
continue;
}
const chat = Number.isInteger(value.chat) && (value.chat as number) > 0
? value.chat as number
: 1;
Expand Down
Loading