diff --git a/.lane/memory/apps/app/src/channel.tsx/01M5JRPBHXDKJV5CM9KV4R35YH-only-this-host-s-own-local-c.md b/.lane/memory/apps/app/src/channel.tsx/01M5JRPBHXDKJV5CM9KV4R35YH-only-this-host-s-own-local-c.md new file mode 100644 index 00000000..5675a271 --- /dev/null +++ b/.lane/memory/apps/app/src/channel.tsx/01M5JRPBHXDKJV5CM9KV4R35YH-only-this-host-s-own-local-c.md @@ -0,0 +1,12 @@ +--- +id: 01M5JRPBHXDKJV5CM9KV4R35YH +anchor: function Channel +created: 2026-10-22T21:36:22Z +norm: '1' +sig: eaede8e100f39ef4 +body_hash: d29a81f75aaca2c5 +raw_hash: 24baf292b9594958 +lines: 305-439 +--- + +only this host's own local channels offer Terminal; saved Terminal tabs elsewhere show where terminals open instead of an empty pane diff --git a/apps/app/src/channel.tsx b/apps/app/src/channel.tsx index 149d5b5a..9e5ffebe 100644 --- a/apps/app/src/channel.tsx +++ b/apps/app/src/channel.tsx @@ -334,9 +334,10 @@ export function Channel( }, [channel.id]); // Shells live on this host, so only its own channels have them; reconnecting may follow a reload. + const elsewhere = remote || place.kind !== "local"; const [shells, setShells] = useState(); useEffect(() => { - if (remote || place.kind !== "local") return; + if (elsewhere) return; let live = true; const load = () => { host.request({ op: "terminals", channel: channel.id }).then( @@ -352,7 +353,7 @@ export function Channel( live = false; off(); }; - }, [channel.id, remote, place.kind]); + }, [channel.id, elsewhere]); const opener = useRef<() => void>(undefined); useEffect(() => { @@ -384,6 +385,7 @@ export function Channel( changed={changed} opener={opener} shells={shells} + terminals={!elsewhere} actions={} render={(data, uid, active, update) => { if (uid === CHAT) { @@ -406,6 +408,18 @@ export function Channel( return ; } if (data.type !== "terminal") return null; + // Tabs saved before Terminal was withheld here still open, so they say why they're empty. + if (elsewhere) { + return ( +
+ {place.kind === "cloud" + ? "This cloud channel's shell is in its container, so terminals don't open here." + : place.kind === "hosted" + ? `This channel runs on ${place.service}, which doesn't open terminals.` + : `Terminals open on the channel's host, ${channel.host}, and only for its owner.`} +
+ ); + } return ( void }; /** Running shells no client shows; each without a tab here opens as a background Terminal tab. */ shells?: { terminal: string; chat?: number }[]; + /** Whether this host opens the channel's shells; only then is Terminal offered. */ + terminals: boolean; }; const MIN = 350; const CHAT_MIN = 450; // The old app also offered Plan, Browse, and Browser Preview; they return when their backends do. const CHOICES: Choice[] = ["chat", "diff", "terminal"]; +const SHELLESS: Choice[] = ["chat", "diff"]; const ICONS: Record = { blank: IconSplitView, chat: IconMessage, @@ -193,14 +196,20 @@ function focus(uid: string) { document.querySelector(`[data-layout-tab="${CSS.escape(uid)}"]`)?.focus(); } -function Blank({ item, onChoose }: { item: Item; onChoose: (choice: Choice) => void }) { +function Blank( + { item, choices, onChoose }: { + item: Item; + choices: Choice[]; + onChoose: (choice: Choice) => void; + }, +) { const Icon = item.icon; if (item.kind === "blank") { return (
- {CHOICES.map((choice) => { + {choices.map((choice) => { const Icon = ICONS[choice]; return (
{more && } {actions} - + {!flat && }
); @@ -694,7 +705,8 @@ type PaneProps = BarProps & { }; function Pane(props: PaneProps) { - const { pane, items, meta, focused, drop, header, render, onFocus, onChoose, onUpdate } = props; + const { pane, items, meta, focused, drop, header, render, choices, onFocus, onChoose, onUpdate } = + props; const hover = drop?.kind === "pane" ? drop : undefined; const blocked = !!hover?.side && !hover.valid; @@ -731,7 +743,13 @@ function Pane(props: PaneProps) { active ? "" : "pointer-events-none invisible" }`} > - {node || onChoose(id, choice)} />} + {node || ( + onChoose(id, choice)} + /> + )} ); })} @@ -1038,6 +1056,7 @@ export function Layout(props: Props) { onFork: (side: "right" | "bottom") => add(pane, "blank", side), can: (side: "right" | "bottom") => allow(pane, side), onSelect: (uid: string) => choose(uid, pane), + choices: props.terminals ? CHOICES : SHELLESS, onAdd: (choice: Choice) => add(pane, choice), onClose: close, onRename: rename,