Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Merge branch 'worktree-agent-ace9d6458cc8f30c0'

syntaqxcommitted Parentsda60fa3d10ea9bBrowse files
4 files+80−280/4 viewed
+2−1
215215 | Section | What's in it |
216216 | --- | --- |
217217 | **Pinned** | Conversations you pinned (hover over one and choose the pin, or right-click it). Only when you have some. |
218−| **Channels** | The channels you're in, by name, then **Browse channels** with how many more you could join. **+** creates one. |
218+| **Channels** | The workspace's own channels you're in, workspace and private, by name, then **Browse channels** with how many more you could join. **+** creates one. |
219+| **Public channels** | The [public channels](#public-channels) you're in, by name, kept apart because anyone on g1t can be in them. Only when you're in some. |
219220 | **Direct messages** | [g1t](#talk-with-g1t) first, then your direct messages, the latest first. The pencil starts a new message. Empty, it says **Message a teammate.** |
220221
221222 Unread conversations are in bold with a count; mentions of you are counted
+41−20
4343 }
4444
4545 /**
46− * Chat mode's sidebar: what you pinned, the channels (and a way to browse
47− * the rest), then direct messages, the latest first. Sections fold, and
46+ * Chat mode's sidebar: what you pinned, the workspace's own channels (and a
47+ * way to browse the rest), its public channels, then direct messages, the
48+ * latest first. Sections fold, and
4849 * stay folded on this device. All, Unread and Mentions filter every section. It refreshes
4950 * itself while the tab is shown, so unread counts move without a reload.
5051 */
8485 // The open conversation stays listed even when it is an old one with an agent alone, which Jump to otherwise finds.
8586 const currentId = entries.find((e) => pathname === channelPath(slug, e.channel) || pathname.startsWith(`${channelPath(slug, e.channel)}/`))?.channel.id ?? null;
8687 const shown = filterEntries(entries, filter, query, currentId);
87− const { pinned, channels, dms } = sections(shown);
88+ const { pinned, channels, publicChannels, dms } = sections(shown);
8889 const loading = !data;
8990
9091 // Folded sections, remembered per workspace on this device. A folded
141142 onChange={(event) => setQuery(event.target.value)}
142143 onKeyDown={(event) => {
143144 if (event.key === "Enter") {
144− const first = [...pinned, ...channels, ...dms][0];
145+ const first = [...pinned, ...channels, ...publicChannels, ...dms][0];
145146 if (first) {
146147 navigate(channelPath(slug, first.channel));
147148 setQuery("");
191192 {pinned.map(conversation)}
192193 </Section>
193194 )}
194− <Section
195− id="channels"
196− title="Channels"
197− folded={folded}
198− onFold={fold}
199− action={sidebar.guest ? undefined : <CreateChannelButton slug={slug} />}
200− keep={channels.filter(keepShown).map(conversation)}
201− >
202− {channels.map(conversation)}
203− {channels.length === 0 && filter === "all" && !query && <Empty>{sidebar.guest ? "You're in none of its channels yet." : "No channels yet."}</Empty>}
204− {filter === "all" && !query && (
205− <ExtraRow to={`/${slug}/-/chat/browse`} icon={<Compass size={15} />} count={sidebar.browsable > 0 ? sidebar.browsable : undefined} countLabel="to join">
206− {sidebar.guest ? "Public channels" : "Browse channels"}
207− </ExtraRow>
208− )}
209− </Section>
195+ {/* A guest has only the channels they were asked into here; with none, the public ones are all there is. */}
196+ {(!sidebar.guest || channels.length > 0) && (
197+ <Section
198+ id="channels"
199+ title="Channels"
200+ folded={folded}
201+ onFold={fold}
202+ action={sidebar.guest ? undefined : <CreateChannelButton slug={slug} />}
203+ keep={channels.filter(keepShown).map(conversation)}
204+ >
205+ {channels.map(conversation)}
206+ {channels.length === 0 && filter === "all" && !query && <Empty>No channels yet.</Empty>}
207+ {!sidebar.guest && filter === "all" && !query && <BrowseRow slug={slug} count={sidebar.browsable} />}
208+ </Section>
209+ )}
210+ {/* Open to anyone on g1t, apart from the workspace's own: only when there are some, or for a guest, to find them. */}
211+ {(publicChannels.length > 0 || (sidebar.guest && filter === "all" && !query)) && (
212+ <Section id="public" title="Public channels" folded={folded} onFold={fold} keep={publicChannels.filter(keepShown).map(conversation)}>
213+ {publicChannels.map(conversation)}
214+ {sidebar.guest && filter === "all" && !query && (
215+ <>
216+ {publicChannels.length === 0 && <Empty>You&apos;re in none of its public channels yet.</Empty>}
217+ <BrowseRow slug={slug} count={sidebar.browsable} guest />
218+ </>
219+ )}
220+ </Section>
221+ )}
210222 {(sidebar.invitations?.length ?? 0) > 0 && filter === "all" && !query && (
211223 <Section id="invitations" title="Invitations" folded={folded} onFold={fold}>
212224 {sidebar.invitations!.map((invitation) => (
341353 );
342354 }
343355
356+/** Browse channels, with how many more there are to join; a guest browses the public ones. */
357+function BrowseRow({ slug, count, guest = false }: { slug: string; count: number; guest?: boolean }) {
358+ return (
359+ <ExtraRow to={`/${slug}/-/chat/browse`} icon={<Compass size={15} />} count={count > 0 ? count : undefined} countLabel="to join">
360+ {guest ? "Browse public channels" : "Browse channels"}
361+ </ExtraRow>
362+ );
363+}
364+
344365 function Empty({ children }: { children: ReactNode }) {
345366 return <li className="px-2 py-1 text-xs text-faint">{children}</li>;
346367 }
+25−3
123123 assert.deepEqual(parsed[2], { t: "code", lang: "ts", v: "const a = 1;" });
124124 });
125125
126−function entry(id: string, over: Partial<ChatSidebarEntry> & { kind?: "channel" | "dm"; last?: string } = {}): ChatSidebarEntry {
126+function entry(id: string, over: Partial<ChatSidebarEntry> & { kind?: "channel" | "dm"; last?: string; visibility?: "private" | "workspace" | "public" } = {}): ChatSidebarEntry {
127127 return {
128128 channel: {
129129 id,
131131 kind: over.kind ?? "channel",
132132 name: over.kind === "dm" ? null : id,
133133 topic: null,
134− private: false,
134+ private: over.visibility === "private",
135+ visibility: over.visibility ?? (over.kind === "dm" ? "private" : "workspace"),
135136 created_by: { kind: "user", id: "u1" },
136137 created_at: "2026-10-01T00:00:00Z",
137138 archived_at: null,
156157 entry("d3", { kind: "dm", title: "ana, reviewer", others: [ana, bot], last: "2026-10-03T00:00:00Z" }),
157158 entry("d4", { kind: "dm", title: "notes", others: [], last: "2026-10-01T00:00:00Z" }),
158159 ];
159− const { pinned, channels, dms } = sections(all);
160+ const { pinned, channels, publicChannels, dms } = sections(all);
160161 assert.deepEqual(pinned.map((e) => e.title), ["general"]);
161162 assert.deepEqual(channels.map((e) => e.title), ["noisy", "random"]);
163+ assert.deepEqual(publicChannels, []);
162164 assert.deepEqual(dms.map((e) => e.title), ["ana", "ana, reviewer", "reviewer", "notes"]);
163165 // An old conversation with an agent alone is left out until searched for or open.
164166 assert.deepEqual(filterEntries(all, "unread", "").map((e) => e.title), ["general", "noisy"]);
167169 assert.deepEqual(unreadTotals(all), { unread: 3, mentions: 1 });
168170 });
169171
172+test("the sidebar keeps public channels apart from the workspace's own", () => {
173+ const all = [
174+ entry("general"),
175+ entry("leads", { visibility: "private", unread: 1 }),
176+ entry("support", { visibility: "public", mentions: 2, unread: 2 }),
177+ entry("announcements", { visibility: "public" }),
178+ entry("launch", { visibility: "public", starred: true }),
179+ entry("d1", { kind: "dm", title: "ana", others: [ana] }),
180+ ];
181+ const { pinned, channels, publicChannels, dms } = sections(all);
182+ // Pinned keeps a public channel you pinned; the rest are by name in their own group.
183+ assert.deepEqual(pinned.map((e) => e.title), ["launch"]);
184+ assert.deepEqual(channels.map((e) => e.title), ["general", "leads"]);
185+ assert.deepEqual(publicChannels.map((e) => e.title), ["announcements", "support"]);
186+ assert.deepEqual(dms.map((e) => e.title), ["ana"]);
187+ // Filters reach both groups.
188+ assert.deepEqual(sections(filterEntries(all, "unread", "")).publicChannels.map((e) => e.title), ["support"]);
189+ assert.deepEqual(sections(filterEntries(all, "mentions", "")).channels, []);
190+});
191+
170192 test("an old conversation with an agent alone opens read-only, under the name the agent had", () => {
171193 const deleted = { ...bot, id: "a9", name: "triage", display_name: "Triage", deleted: true };
172194 const all = [
+12−4
77 * Message bodies are never HTML: they are parsed here into a small tree
88 * that React renders as text, so nothing anyone writes can run.
99 */
10−import type { ChatMessage, ChatSidebarEntry, MemberProfile, Principal } from "@g1t/contracts";
10+import type { Channel, ChatMessage, ChatSidebarEntry, MemberProfile, Principal } from "@g1t/contracts";
1111
1212 /** Two messages from the same author this close together share one header. */
1313 export const GROUP_GAP_MS = 5 * 60 * 1000;
182182
183183 /**
184184 * The sidebar's sections, in the order they show: what is pinned (the
185− * `starred` preference), channels by name, then direct messages, the
186− * latest first.
185+ * `starred` preference), the workspace's own channels (workspace and
186+ * private) by name, its public channels by name, then direct messages, the
187+ * latest first. A pinned channel stays under Pinned, whatever it is.
187188 */
188189 export function sections(entries: ChatSidebarEntry[]): {
189190 pinned: ChatSidebarEntry[];
190191 channels: ChatSidebarEntry[];
192+ publicChannels: ChatSidebarEntry[];
191193 dms: ChatSidebarEntry[];
192194 } {
193195 const byName = (a: ChatSidebarEntry, b: ChatSidebarEntry) => a.title.localeCompare(b.title);
196198 (b.channel.last_message_at ?? b.channel.created_at).localeCompare(a.channel.last_message_at ?? a.channel.created_at);
197199 return {
198200 pinned: entries.filter((e) => e.starred).sort(byName),
199− channels: entries.filter((e) => !e.starred && e.channel.kind === "channel").sort(byName),
201+ channels: entries.filter((e) => !e.starred && e.channel.kind === "channel" && !isPublic(e.channel)).sort(byName),
202+ publicChannels: entries.filter((e) => !e.starred && e.channel.kind === "channel" && isPublic(e.channel)).sort(byName),
200203 // Your direct message with @g1t heads the list, whenever you last talked.
201204 dms: entries.filter((e) => !e.starred && e.channel.kind === "dm").sort((a, b) => Number(g1tEntry(b)) - Number(g1tEntry(a)) || byRecent(a, b)),
202205 };
203206 }
204207
208+/** Open to anyone on g1t, not only the workspace's members. */
209+function isPublic(channel: Pick<Channel, "private" | "visibility">): boolean {
210+ return !channel.private && channel.visibility === "public";
211+}
212+
205213 /** Whether a sidebar entry is the viewer's direct message with @g1t. */
206214 export function g1tEntry(entry: { channel: { kind: "channel" | "dm" }; others: readonly Pick<MemberProfile, "kind" | "id">[] }): boolean {
207215 return entry.channel.kind === "dm" && entry.others.length === 1 && entry.others[0]!.kind === "system" && entry.others[0]!.id === "g1t";

Comments

Sign in to comment.