mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Each branch is a top-level conversation forked from another at an entry; the session-scoped p2.main document lists them and names the current one. Earlier branches are read-only. The main agent's per-agent state (subagents, artifacts, voice and chat mode, pending move) stays on conversation 1 for every branch. - tree plugin: SessionTree (tree, navigate, select); the worker reports main and branches to the catalog (Sessions v4, catalog schema 4) - TUI: /tree and double Escape (pi's default "tree"), a tree selector adapted from coding-agent - web: Edit on user messages, Branch from here on answers, branch rows under the session in the sidebar - main-agent checks use the branch list instead of conversation 1 - search indexes each branch's own entries Design: apps/p2/docs/tree.md
78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
import { defineFacet, forbidden, isOwner, nativeOf, serveNative } from "@p2/app/v1";
|
|
import { Server, serverOf, Upgrades } from "@p2/app/v1/server";
|
|
import { Catalog } from "../sessions/contract.ts";
|
|
import { ChildStore, conversationLocation } from "../subagents/contract.ts";
|
|
import { TerminalServiceObject, TerminalServices, Terminals } from "./contract.ts";
|
|
import type { TerminalRequest } from "./protocol.ts";
|
|
import { TerminalService } from "./service.ts";
|
|
|
|
export default defineFacet((host) => {
|
|
serveNative(host, TerminalServices);
|
|
host.serve(TerminalServiceObject, (_params, self) => {
|
|
const server = self.get(Server, {});
|
|
const serviceResource = self.open(
|
|
async () => new TerminalService(server.info.value.dataDir, server.info.value.app),
|
|
(service) => service.close(),
|
|
);
|
|
self.contribute(TerminalServices, {
|
|
get service() {
|
|
return serviceResource.value;
|
|
},
|
|
});
|
|
self.contribute(Upgrades, {
|
|
"/terminal": (request, socket, head, principal) => {
|
|
if (!isOwner(principal)) {
|
|
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
return;
|
|
}
|
|
serviceResource.value.upgrade(request, socket, head);
|
|
},
|
|
});
|
|
return {};
|
|
});
|
|
|
|
host.serve(Terminals, ({ session: id }, principal, self) => {
|
|
if (!isOwner(principal)) throw forbidden("terminals are owner-only");
|
|
self.get(TerminalServiceObject, {});
|
|
const services = nativeOf(self, TerminalServices, "The terminal service");
|
|
const service = () => services().service;
|
|
const catalog = self.get(Catalog, {});
|
|
const children = self.get(ChildStore, {});
|
|
const server = serverOf(self);
|
|
const viewResource = self.open(
|
|
async () => {
|
|
await server().root((ctx) => catalog.get({ id }, ctx));
|
|
return service().open(id);
|
|
},
|
|
(view) => service().release(view),
|
|
);
|
|
/** Each call is one request of the terminal host's protocol, for this session's view. */
|
|
const command = async (request: TerminalRequest): Promise<null> => {
|
|
await viewResource.value.command(request);
|
|
return null;
|
|
};
|
|
return {
|
|
variant: "owner",
|
|
members: {
|
|
channel: async () => service().channel(viewResource.value),
|
|
list: () => command({ type: "list", sessionId: id }),
|
|
open: async ({ cols, rows, conversation }, ctx) => {
|
|
const session = await catalog.get({ id }, ctx);
|
|
if (session.archivedAt !== null) throw new Error("Session is archived");
|
|
// Where the conversation works: a subagent's own location stays where it is when the session moves.
|
|
const child = children.children.value[id]?.find((each) => each.conversationId === conversation);
|
|
if (conversation !== 1 && conversation !== session.main && !child)
|
|
throw new Error("Unknown conversation");
|
|
const { cwd, env } = conversationLocation(session, child)!;
|
|
return command({ type: "open", sessionId: id, cwd, env, cols, rows });
|
|
},
|
|
attach: ({ terminalId }) => command({ type: "attach", terminalId }),
|
|
close: ({ force }) => command({ type: "close", force }),
|
|
resize: ({ cols, rows }) => command({ type: "resize", cols, rows }),
|
|
theme: (theme) => command({ type: "theme", ...theme }),
|
|
ack: ({ bytes }) => command({ type: "ack", bytes }),
|
|
restartHost: () => command({ type: "shutdown" }),
|
|
},
|
|
};
|
|
});
|
|
});
|