Skip to content
Closed
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
2 changes: 1 addition & 1 deletion docs/design/PRODUCTION-READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ record; current behaviour is in the linked [architecture](../architecture.md) do
| mecak8s (storage-free k8s-native agent) | ✅ shipped (MVP) · ✅ OPT-IN `/metrics` loopback scrape + OTLP push (ADR 0098) · ✅ verified external Redis TLS/ACL with transactional projected-file reload + last-valid generations (ADR 0240) · ✅ Helm 0.3.0 secure real-provider in-pod TLS+OIDC or edge-terminated TLS+OIDC (ClusterIP h2c), nullable spend ceilings, and pod scheduling controls · ⛔ CRD/Operator · ⛔ HPA (custom-metrics on active-runs) · ⛔ managed Redis provisioning (ElastiCache/MemoryStore — endpoint only) · ⛔ fix `mecated`'s unbounded `GracefulStop` (pre-existing, follow-up) | [mecak8s.md](../adr/0048-mecak8s.md) · [0098](../adr/0098-headless-telemetry.md) · [0240](../adr/0240-mecak8s-credential-reload-and-chart-security.md) · [0278](../adr/0278-mecak8s-edge-terminated-tls.md) · [MECAK8S-PLAN.md](./MECAK8S-PLAN.md) | [overview](../architecture.md) |
| ACP adapter (editor stdio surface) | ✅ Phase 1+2 + bounded Phase 3 + multimodal shipped · ⛔ Phase 3 long-tail (rule persistence, grep-over-buffers, fs/* on resume) | [0001-acp-adapter.md](../adr/0001-acp-adapter.md) | [api surface](../architecture/api-surface.md) |
| Conversation fork (peer session from a history snapshot) | ✅ shipped · ✅ effort override (mid-conversation effort switch, keeps the transcript — [0068](../adr/0068-effort-change-via-fork.md)) · ⛔ cross-provider/model fork (v2: replay-blob stripping) · ⛔ workspace-branching fork · ⛔ fork-from-event-log-at-arbitrary-point · ⛔ fork lineage (`forked_from` label) | [0065-conversation-fork.md](../adr/0065-conversation-fork.md) | [overview](../architecture.md) |
| Studio (web client) | 🚧 landing as a stacked PR series: ✅ module foundation (vendored Atrium UI kit, toolchain, CI gates) · ✅ server tier (trusted proxy + managed-mode controller core, hermetic suite) · ✅ protocol seam + harness transport · ✅ workspace shell + runtime status · ✅ Chats core + hermetic browser e2e (fixture daemon) · ✅ Scheduled (authoring, carried-spec edit, fire log + per-fire transcripts) · ✅ Skills (browse/create/upload/enable-disable, controller-mediated; learned-skills panel) · ✅ Memory (read-only table + detail + consolidate, honest disabled/empty states) · ✅ Settings core (Personalize, agent identity, learning review) · ✅ provider management + model router + MCP gateway (controller-mediated; server-side auth.yaml ownership, no key material on the wire) · ✅ external-mode OIDC sign-in (PKCE, server-held tokens) · ✅ advanced chat: attachments/previews, tool drill-down + context meter + typed retry + live delegation cards, queue + steer, threads + canvas, live re-attach via the session watch (residual: the driving tab's prompt stream still cancels on disconnect) · ✅ session modes + fork-based mid-chat model switch + consented debug-with-AI (ADR 0254) · ⛔ Labs · ⛔ mobile round · ⛔ advanced chat tiers (attachments, steer/queue, threads, re-attach, modes, mobile) | [0288](../adr/0288-studio-atrium-module.md) · [0289](../adr/0289-studio-server-backed-chats.md) | [overview](../architecture.md) |
| Studio (web client) | 🚧 landing as a stacked PR series: ✅ module foundation (vendored Atrium UI kit, toolchain, CI gates) · ✅ server tier (trusted proxy + managed-mode controller core, hermetic suite) · ✅ protocol seam + harness transport · ✅ workspace shell + runtime status · ✅ Chats core + hermetic browser e2e (fixture daemon) · ✅ Scheduled (authoring, carried-spec edit, fire log + per-fire transcripts) · ✅ Skills (browse/create/upload/enable-disable, controller-mediated; learned-skills panel) · ✅ Memory (read-only table + detail + consolidate, honest disabled/empty states) · ✅ Settings core (Personalize, agent identity, learning review) · ✅ provider management + model router + MCP gateway (controller-mediated; server-side auth.yaml ownership, no key material on the wire) · ✅ external-mode OIDC sign-in (PKCE, server-held tokens) · ✅ advanced chat: attachments/previews, tool drill-down + context meter + typed retry + live delegation cards, queue + steer, threads + canvas, live re-attach via the session watch (residual: the driving tab's prompt stream still cancels on disconnect) · ✅ session modes + fork-based mid-chat model switch + consented debug-with-AI (ADR 0254) · ✅ Labs mock Projects + feature tour (default-off; mock ids hard-gated from the daemon) · ⛔ mobile round · ⛔ advanced chat tiers (attachments, steer/queue, threads, re-attach, modes, mobile) | [0288](../adr/0288-studio-atrium-module.md) · [0289](../adr/0289-studio-server-backed-chats.md) | [overview](../architecture.md) |
| _Historical / retired_ | — | [ARCHITECTURE.md](../adr/0004-v1-architecture.md) · [STEP-CHAIN.md](../adr/0006-v1-step-chain.md) · [TWELVE-PATTERNS-AUDIT.md](../adr/0007-twelve-patterns-audit.md) · [REPOMAP-TREE-SITTER.md](../adr/0029-repomap-tree-sitter.md) | — |

## Security
Expand Down
101 changes: 101 additions & 0 deletions studio/src/app/workspace/_components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
Bot,
Check,
ChevronDown,
FolderClosed,
FolderPlus,
Mic,
Paperclip,
Plus,
Expand All @@ -19,7 +21,9 @@ import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
Expand All @@ -30,6 +34,7 @@ import {
getAgentMentions,
getSlashCommands,
} from "@/features/agent/composer-capabilities";
import { usePrompt } from "@/hooks/use-prompt";
import { fileKindMeta } from "@/lib/file-meta";
import {
type EnterSendBehavior,
Expand All @@ -44,9 +49,19 @@ import {
setComposerText,
} from "./composer-mentions";

interface ProjectItem {
id: string;
name: string;
color: string;
}

interface ChatInputProps {
placeholder?: string;
rows?: number;
projects?: ProjectItem[];
selectedProjectId?: string | null;
onSelectProject?: (id: string | null) => void;
onCreateProject?: (name: string) => void;
compact?: boolean;
onSend?: (content: string, files?: File[]) => void;
onQueue?: (content: string) => void;
Expand Down Expand Up @@ -473,6 +488,80 @@ function MemoryToggle() {
);
}

function ProjectsDropdown({
projects,
selectedProjectId,
onSelect,
onCreate,
}: {
projects?: ProjectItem[];
selectedProjectId?: string | null;
onSelect?: (id: string | null) => void;
onCreate?: (name: string) => void;
}) {
const { prompt, PromptDialog } = usePrompt();
const selected = projects?.find((p) => p.id === selectedProjectId);

return (
<>
{PromptDialog}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" className={GHOST_TRIGGER_CLASS}>
<FolderClosed className="size-3.5 text-muted-foreground" />
{selected ? selected.name : "Project"}
<ChevronDown className="size-3.5 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
onCloseAutoFocus={(e) => e.preventDefault()}
align="start"
className="w-56"
>
<DropdownMenuLabel>Projects</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => onSelect?.(null)}>
<span className={cn(!selectedProjectId && "font-semibold")}>
Chats
</span>
</DropdownMenuItem>
{projects?.map((p) => (
<DropdownMenuItem key={p.id} onClick={() => onSelect?.(p.id)}>
<FolderClosed className="size-3.5 shrink-0 mr-1.5 text-muted-foreground" />
<span
className={cn(
"truncate",
p.id === selectedProjectId && "font-semibold",
)}
>
{p.name}
</span>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={async () => {
const name = await prompt({
title: "New project",
description:
"Give your project a name to organize related conversations.",
placeholder: "Project name",
confirmText: "Create project",
});
if (name) onCreate?.(name);
}}
>
<FolderPlus className="size-3.5 mr-2" />
New project
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}

function useVoiceInput(onTranscript: (text: string) => void) {
const [isListening, setIsListening] = useState(false);
const [isSupported, setIsSupported] = useState(false);
Expand Down Expand Up @@ -723,6 +812,10 @@ function handleMenuNavKey(

export function ChatInput({
placeholder: placeholderProp,
projects,
selectedProjectId,
onSelectProject,
onCreateProject,
compact = false,
onSend,
onQueue,
Expand Down Expand Up @@ -1167,6 +1260,14 @@ export function ChatInput({
container queries when THIS row runs narrow (a ~400px side-panel
composer), independent of the viewport width. */}
<div className="@container -mt-4 pt-5 px-2 pb-1.5 flex items-center gap-1 rounded-b-2xl border border-t-0 border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 overflow-x-auto hide-scrollbar">
{projects && (
<ProjectsDropdown
projects={projects}
selectedProjectId={selectedProjectId}
onSelect={onSelectProject}
onCreate={onCreateProject}
/>
)}
{!compact && (
<>
{onModeChange && (
Expand Down
76 changes: 73 additions & 3 deletions studio/src/app/workspace/chat/_components/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
useAgentChat,
} from "@/features/agent";
import type { QueuedMessage } from "@/features/agent/hooks/use-agent-chat";
import { isMockTourSession } from "@/features/agent/mock-tour";
import { formatTokens } from "@/lib/formatters";
import {
createThreadHarnessSession,
Expand Down Expand Up @@ -368,6 +369,9 @@ function ThreadPanel({

const handleSend = useCallback(
async (content: string, files?: File[]) => {
// Defense in depth: a mock session id must never mint a daemon thread
// session (the panel router already sends mock threads elsewhere).
if (isMockTourSession(parentSessionId)) return;
setSourceBusy(false);
setCreateError(null);
if (threadIdRef.current) {
Expand Down Expand Up @@ -550,6 +554,57 @@ function ThreadPanel({
);
}

/**
* The thread panel for the Labs mock chat: a read-only view of the root
* message's canned replies. Entirely local — a mock session id must never
* mint a daemon thread session, so this replaces ThreadPanel outright.
*/
function MockThreadPanel({
rootMessage,
botName,
onClose,
maximized,
onToggleMaximize,
}: {
rootMessage: AgentMessage;
botName: string;
onClose: () => void;
maximized: boolean;
onToggleMaximize: () => void;
}) {
const replies = rootMessage.replies ?? [];
return (
<SidePanel
icon={MessageSquareText}
title="Thread"
closeLabel="Close thread"
maximized={maximized}
onToggleMaximize={onToggleMaximize}
onClose={onClose}
minWidth={340}
>
<div className="flex-1 min-h-0 overflow-y-auto px-3 pt-3 pb-4 lg:px-4">
<div className="rounded-lg border border-dashed border-border/70 px-1 py-1">
<MessageBubble message={rootMessage} botName={botName} />
</div>
{replies.length > 0 && (
<div className="my-2 flex items-center gap-2 px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">
<span className="h-px flex-1 bg-border" />
{replies.length} {replies.length === 1 ? "reply" : "replies"}
<span className="h-px flex-1 bg-border" />
</div>
)}
{replies.map((reply) => (
<MessageBubble key={reply.id} message={reply} botName={botName} />
))}
<p className="px-2 pt-2 text-xs text-muted-foreground">
Mock thread &mdash; read-only demo content.
</p>
</div>
</SidePanel>
);
}

function TextSelectionToolbar({
containerRef,
onAddToChat,
Expand Down Expand Up @@ -690,6 +745,7 @@ export function ChatView({
onCancelRun,
onCompact,
contextInfo,
readOnlyPlaceholder,
mode,
onModeChange,
models,
Expand Down Expand Up @@ -746,6 +802,9 @@ export function ChatView({
/** The session's effective model + context window (B1.1): feeds the slim
approximate context meter near the composer. */
contextInfo?: { modelLabel: string; contextWindow: number } | null;
/** Disables the composer and shows this placeholder instead (the Labs
mock chat is read-only demo content). */
readOnlyPlaceholder?: string;
/** The session's current permission mode, for the composer's Mode selector. */
mode?: SessionPermissionMode;
/** Renders the composer's Mode selector when provided (the mock tour chat
Expand Down Expand Up @@ -1131,7 +1190,7 @@ export function ChatView({
mode={mode}
onModeChange={onModeChange}
isStreaming={isStreaming}
disabled={!!pendingApproval}
disabled={!!pendingApproval || readOnlyPlaceholder != null}
appendText={appendText}
onAppendConsumed={handleAppendConsumed}
initialText={editSeed ?? initialDraft}
Expand All @@ -1140,13 +1199,14 @@ export function ChatView({
else onInitialDraftConsumed?.();
}}
placeholder={
isStreaming
readOnlyPlaceholder ??
(isStreaming
? // "Steer" is only an honest promise while the daemon
// actually supports it (C1.2) — absent, sends queue.
enterBehavior === "steer" && onSteerMessage
? "Steer the agent..."
: "Queue a message..."
: "Send a message..."
: "Send a message...")
}
/>
)}
Expand Down Expand Up @@ -1196,6 +1256,16 @@ function SidePanelForKind({
case "toolcall":
return <ToolCallPanel call={panel.call} {...shared} />;
case "thread":
// Mock chat threads stay local: read-only replies, no daemon session.
if (isMockTourSession(parentSessionId)) {
return (
<MockThreadPanel
rootMessage={panel.message}
botName={botName}
{...shared}
/>
);
}
return (
<ThreadPanel
// Re-key per root message: switching threads must remount the
Expand Down
Loading
Loading