Skip to content
Open
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
4 changes: 3 additions & 1 deletion apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { useThreadListActions } from "./useThreadListActions";
import { HomeScopeFilters } from "./HomeScopeFilters";
import type { HomeHeaderProps } from "./HomeHeader.types";
import { useRockBotMenu } from "../rockbot/useRockBotMenu";
import { isRockBot } from "@t3tools/client-runtime/state/rockbot";

/* ─── Route screen ───────────────────────────────────────────────────── */

Expand Down Expand Up @@ -101,7 +102,8 @@ export function HomeRouteScreen() {
environmentId: selectedEnvironmentId,
projectGroupingMode: listOptions.projectGroupingMode,
}),
threads,
// RockBot chats live in No project only as storage; they must not reorder it.
threads: threads.filter((thread) => !isRockBot(thread)),
pendingTasks,
projectSortOrder: listOptions.projectSortOrder,
}).map((scope) => ({
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/src/features/home/homeTerminalTargets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isRockBot } from "@t3tools/client-runtime/state/rockbot";
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/viewcodeThread";


import { getThreadSortTimestamp } from "@t3tools/client-runtime/viewcodeThreadSort";
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";

Expand Down Expand Up @@ -38,7 +38,8 @@ export function buildHomeTerminalTargets(input: {
{ readonly thread: EnvironmentThreadShell; readonly timestamp: number }
>();
for (const thread of input.threads) {
if (thread.archivedAt !== null) continue;
// A RockBot chat runs in the scratch folder only to stay out of every repository.
if (thread.archivedAt !== null || isRockBot(thread)) continue;
const scopeKey = scopeKeyByProjectKey.get(
scopedProjectKey(thread.environmentId, thread.projectId),
);
Expand Down
14 changes: 9 additions & 5 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1855,6 +1855,9 @@ export default function ChatView(props: ChatViewProps) {
? scopeProjectRef(activeThreadEnvironmentId, activeThreadProjectId)
: null;
const activeProject = useProject(activeProjectRef);
// RockBot chats live in the scratch folder only to keep their agent out of every
// repository; the header never offers that folder's project tools.
const isRockBotChat = serverThread !== null && isRockBot(serverThread.source);
const activeProjectSettings = resolveProjectSettings(
settings,
activeProject?.id ?? null,
Expand Down Expand Up @@ -8055,16 +8058,17 @@ export default function ChatView(props: ChatViewProps) {
activeThreadId={activeThread.id}
isServerThread={isServerThread}
activeThreadTitle={activeThread.title}
activeProject={activeProject ?? null}
activeProject={isRockBotChat ? null : (activeProject ?? null)}
isRockBot={isRockBotChat}
parentThreadLink={parentThreadLink}
onOpenThread={onOpenRelatedThreadStable}
rightPanelOpen={inlineRightPanelOwnsTitleBar}
toolbarControlCount={inlineRightPanelOwnsTitleBar ? 1 : 0}
actions={
<div data-chat-header-actions className="flex shrink-0 items-center gap-2">
<ViewCodeHeaderProjectActions
projectCwd={activeProject?.workspaceRoot ?? null}
{...(showOpenInPicker
projectCwd={isRockBotChat ? null : (activeProject?.workspaceRoot ?? null)}
{...(showOpenInPicker && !isRockBotChat
? {
openIn: {
environmentId: activeThread.environmentId,
Expand Down Expand Up @@ -8107,7 +8111,7 @@ export default function ChatView(props: ChatViewProps) {
isServerThread={isServerThread}
isSidechat={activeThreadShell?.source.kind === "sidechat"}
keybindings={keybindings}
terminalAvailable={activeProject !== null && canOperateTerminal}
terminalAvailable={activeProject !== null && !isRockBotChat && canOperateTerminal}
terminalOpen={terminalUiState.terminalOpen}
terminalShortcutLabel={shortcutLabelForCommand(keybindings, "terminal.toggle")}
onToggleTerminal={toggleTerminalVisibilityStable}
Expand Down Expand Up @@ -8373,7 +8377,7 @@ export default function ChatView(props: ChatViewProps) {
: undefined
}
>
{serverThread !== null && isRockBot(serverThread.source) ? (
{isRockBotChat ? (
<RockBotHeroHeadline />
) : (
<DraftHeroHeadline
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1312,7 +1312,12 @@ function OpenCommandPaletteDialog(props: {
null;
return (
<ThreadCommandSubtitle
project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null}
project={
// RockBot chats are titled RockBot, not by the scratch folder they run in.
thread.kind === "orchestrator"
? null
: (projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null)
}
projectTitle={projectTitle ?? null}
environmentLabel={
projectEnvironmentLocationById.get(thread.environmentId)?.label ?? "Remote"
Expand Down
20 changes: 14 additions & 6 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,16 @@ export default function Sidebar() {
});
const attentionCounts = (() =>
countSidebarAttention(visibleThreads, isThreadUnseen, agentRecordOf))();
// A RockBot chat sits in the scratch folder only to keep its agent out of every
// repository, so rows name RockBot rather than "No project".
const projectOfThread = (thread: SidebarThreadSummary) =>
thread.kind === "orchestrator"
? null
: (projectByKey.get(physicalProjectKeyOf(thread)) ?? null);
const projectNameOfThread = (thread: SidebarThreadSummary) =>
thread.kind === "orchestrator"
? "RockBot"
: (projectDisplayNameByKey.get(physicalProjectKeyOf(thread)) ?? null);
// RockBot's chats stand in for the project tree while one of them is open.
const rockBotChats = (() => rockBotChatsForRoute(threads, routeThreadRef))();
const openRockBot = useOpenRockBot(rockBotChats?.[0]?.environmentId ?? null);
Expand Down Expand Up @@ -3690,8 +3700,8 @@ export default function Sidebar() {
currentEnvironmentId={primaryEnvironmentId}
environmentLabel={environmentLabelById.get(thread.environmentId) ?? null}
environmentMachine={environmentMachineById.get(thread.environmentId) ?? "server"}
project={projectByKey.get(physicalProjectKeyOf(thread)) ?? null}
projectDisplayName={projectDisplayNameByKey.get(physicalProjectKeyOf(thread)) ?? null}
project={projectOfThread(thread)}
projectDisplayName={projectNameOfThread(thread)}
providerEntryByInstanceId={
providerEntriesByEnvironment.get(thread.environmentId) ?? EMPTY_PROVIDER_ENTRIES
}
Expand Down Expand Up @@ -3974,10 +3984,8 @@ export default function Sidebar() {
key={threadKey}
thread={thread}
nowMinute={nowMinute}
project={projectByKey.get(physicalProjectKeyOf(thread)) ?? null}
projectDisplayName={
projectDisplayNameByKey.get(physicalProjectKeyOf(thread)) ?? null
}
project={projectOfThread(thread)}
projectDisplayName={projectNameOfThread(thread)}
environmentLabel={environmentLabelById.get(thread.environmentId) ?? null}
environmentMachine={
environmentMachineById.get(thread.environmentId) ?? "server"
Expand Down
12 changes: 9 additions & 3 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type ThreadGoal,
} from "@t3tools/contracts";
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import { BotIcon } from "lucide-react";
import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
import {
isAtomCommandInterrupted,
Expand Down Expand Up @@ -45,6 +46,8 @@ interface ChatHeaderProps {
onRunThreadGoal?: () => void;
isServerThread: boolean;
activeProject: EnvironmentProject | null;
/** A RockBot chat: its tab carries RockBot's glyph, since its folder names no project. */
isRockBot?: boolean;
parentThreadLink: { threadId: ThreadId; title: string } | null;
onOpenThread: (threadId: ThreadId) => void;
rightPanelOpen: boolean;
Expand Down Expand Up @@ -81,6 +84,7 @@ export const ChatHeader = memo(function ChatHeader({
onRunThreadGoal,
isServerThread,
activeProject,
isRockBot = false,
rightPanelOpen,
actions,
toolbarControlCount,
Expand Down Expand Up @@ -236,9 +240,11 @@ export const ChatHeader = memo(function ChatHeader({
}
};
const activeTabExtras: ActiveTabExtras = {
...(activeProject
? { glyph: <ProjectFavicon project={activeProject} className="size-3.5 shrink-0" /> }
: {}),
...(isRockBot
? { glyph: <BotIcon aria-hidden className="size-3.5 shrink-0 text-muted-foreground" /> }
: activeProject
? { glyph: <ProjectFavicon project={activeProject} className="size-3.5 shrink-0" /> }
: {}),
...(renamingTitle === null
? {}
: {
Expand Down
30 changes: 25 additions & 5 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { useScopedSettingsWriteAllowed } from "./useScopedSettings";
import { Spinner } from "~/components/ui/spinner";
import { NotificationSettings } from "./NotificationSettings";
import { PRIVACY_POLICY_URL } from "../../legalLinks";
import { ArchiveIcon, ArchiveX, CheckIcon, ChevronRightIcon, SettingsIcon } from "lucide-react";
import {
ArchiveIcon,
ArchiveX,
BotIcon,
CheckIcon,
ChevronRightIcon,
SettingsIcon,
} from "lucide-react";
import { Link, useNavigate } from "@tanstack/react-router";
import type { CSSProperties, ReactNode } from "react";
import { useEffect, useEffectEvent, useRef, useState } from "react";
Expand Down Expand Up @@ -1436,8 +1443,7 @@ export function AppearanceSettingsPanel() {
</div>
}
resetAction={
settings.panelAnimationMs !==
DEFAULT_UNIFIED_SETTINGS.panelAnimationMs ? (
settings.panelAnimationMs !== DEFAULT_UNIFIED_SETTINGS.panelAnimationMs ? (
<SettingResetButton
label="panel animations"
onClick={() =>
Expand Down Expand Up @@ -3441,12 +3447,26 @@ export function ArchivedThreadsPanel() {
readonly project: (typeof archivedProjects)[number];
readonly title: string;
readonly threads: Array<(typeof threads)[number]>;
readonly rockBot?: true;
}
>();
for (const project of archivedProjects) {
const projectThreads: Array<(typeof threads)[number]> = [];
for (const thread of threads) {
if (thread.projectId === project.id && thread.environmentId === project.environmentId) {
// RockBot chats run in the scratch folder but are filed under RockBot.
if (thread.source.kind === "orchestrator") {
const rockBot = groups.get("rockbot");
if (rockBot) rockBot.threads.push(thread);
else
groups.set("rockbot", {
project,
title: "RockBot",
threads: [thread],
rockBot: true,
});
continue;
}
projectThreads.push(thread);
}
}
Expand Down Expand Up @@ -3558,12 +3578,12 @@ export function ArchivedThreadsPanel() {
/>
</SettingsSection>
) : (
archivedGroups.map(({ key, project, title, threads: projectThreads }, index) => (
archivedGroups.map(({ key, project, title, threads: projectThreads, rockBot }, index) => (
<SettingsSection
key={key}
id={index === 0 ? searchableSetting("archive").id : undefined}
title={title}
icon={<ProjectFavicon project={project} />}
icon={rockBot ? <BotIcon /> : <ProjectFavicon project={project} />}
>
{projectThreads.map((thread) => (
<SettingsRow
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/sidebar/useSidebarProjectGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environmen
import { useProjects, useThreadShells } from "../../state/entities";
import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../../uiStateStore";
import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "../Sidebar.logic";
import { isRockBot } from "@t3tools/client-runtime/state/rockbot";

/**
* The sidebar's project folders with their names and members, not yet in sidebar order. For
Expand Down Expand Up @@ -53,5 +54,12 @@ export function useSidebarProjectGroups(): ReadonlyArray<SidebarProjectSnapshot>
const unsortedGroups = useSidebarProjectSnapshots();
const threads = useThreadShells();
const sortOrder = useClientSettings((settings) => settings.sidebarProjectSortOrder);
return (() => sortLogicalProjectsForSidebar(unsortedGroups, threads, sortOrder))();
// RockBot chats live in No project only as storage; chatting with RockBot must not
// move that folder up.
return (() =>
sortLogicalProjectsForSidebar(
unsortedGroups,
threads.filter((thread) => !isRockBot(thread.source)),
sortOrder,
))();
}
5 changes: 4 additions & 1 deletion docs/internals/viewcode.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ inside a repository, borrowing the latest project only where Scratch is unavaila
and start on the latest thread's model. Project lists hide them: the web sidebar
swaps its project tree for the chat list while one is open, the rail lights RockBot
instead of a folder, and a folder pick leaves RockBot. Settlement candidates exclude
them.
them. Scratch is only their storage, and No project stays the user's own place for
chats without a repository: a RockBot chat's tab, rows, palette entry and mobile
header say RockBot, its header offers no project tools, and RockBot activity does
not reorder the No project folder.

Its safety is the server's, not its prompt's. `McpToolAccess` refuses every writing
tool to an orchestrator caller except the `sendsForUser` ones (`t3_thread_send`,
Expand Down
3 changes: 2 additions & 1 deletion docs/user/rockbot.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ in its composer like any thread. Each computer you connect has its own RockBot.
While a RockBot chat is open, the sidebar lists your RockBot chats in place of your
projects, newest first. Start another with the pencil next to **RockBot**, or **New
RockBot chat** in the command palette. Rename or archive them like any thread; archived
chats are under **Settings → Archived**. Click a project on the rail to go back to your
chats are under **RockBot** in **Settings → Archived**. RockBot chats never appear under
**No project**, which stays the place for chats you start without a project. Click a project on the rail to go back to your
work.

On mobile (iOS and Android), the sparkle button on Home opens a menu with **New chat**
Expand Down
34 changes: 34 additions & 0 deletions packages/client-runtime/src/state/rockbot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vite-plus/test";

import { rockBotChatsIn } from "./rockbot.ts";

const thread = (
id: string,
extra: {
kind?: string | null;
archivedAt?: string | null;
updatedAt?: string;
} = {},
) => ({
environmentId: "local",
id,
kind: null,
archivedAt: null,
updatedAt: "2026-10-20T10:00:00Z",
...extra,
});
describe("rockBotChatsIn", () => {
it("lists one environment's live RockBot chats, newest first", () => {
expect(
rockBotChatsIn(
[
thread("rockbot", { kind: "orchestrator", updatedAt: "2026-10-20T09:00:00Z" }),
thread("rockbot:2", { kind: "orchestrator", updatedAt: "2026-10-20T12:00:00Z" }),
thread("rockbot:gone", { kind: "orchestrator", archivedAt: "2026-10-20T13:00:00Z" }),
thread("work"),
],
"local",
).map((chat) => chat.id),
).toEqual(["rockbot:2", "rockbot"]);
});
});
Loading