Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
08ca316
🤖 feat: Instructions tab in right sidebar
ammar-agent May 10, 2026
598ca40
🤖 refactor: unify right-sidebar tab registry; add Instructions to def…
ammar-agent May 10, 2026
d881d1c
🤖 fix: parent AGENTS.md for sub-project workspaces; align Instruction…
ammar-agent May 10, 2026
00a958e
🤖 fix: keep Instructions tab default-visible and unblock CI
ammar-agent May 10, 2026
07501b1
🤖 feat: workspace additional system context scratchpad
ammar-agent May 10, 2026
d9c68b8
🤖 fix: mock additional system context in Storybook
ammar-agent May 10, 2026
8b2b643
🤖 fix: use live scratchpad context for sends
ammar-agent May 10, 2026
0e9b898
🤖 fix: isolate scratchpad save generations
ammar-agent May 10, 2026
f42cdaa
🤖 fix: send live scratchpad separately
ammar-agent May 10, 2026
38e270e
🤖 fix: keep scratchpad save queue across unmounts
ammar-agent May 10, 2026
02ad108
🤖 fix: hydrate scratchpad before live override
ammar-agent May 10, 2026
2abd728
🤖 fix: ignore stale scratchpad save responses
ammar-agent May 10, 2026
24f6b9a
🤖 tests: update sidebar archive layout expectation
ammar-agent May 10, 2026
7825079
🤖 tests: account for Instructions tab in sidebar e2e
ammar-agent May 10, 2026
5a60c99
🤖 feat: count badge for Instructions tab label
ammar-agent May 10, 2026
ce8eadf
🤖 feat: Chat Instructions toggle + chat decoration + delayed save flash
ammar-agent May 12, 2026
344b36d
🤖 refactor: Chat Instructions decoration as link + orange dirty asterisk
ammar-agent May 12, 2026
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: 2 additions & 0 deletions src/browser/components/ChatPane/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
import { Lightbulb } from "lucide-react";
import { MessageListProvider } from "@/browser/features/Messages/MessageListContext";
import { cn } from "@/common/lib/utils";
import { AdditionalSystemContextChatDecoration } from "@/browser/components/InstructionsTab/AdditionalSystemContextScratchpad";
import { MessageRenderer } from "@/browser/features/Messages/MessageRenderer";
import { MarkdownRenderer } from "@/browser/features/Messages/MarkdownRenderer";
import { useTranscriptContextMenu } from "@/browser/features/Messages/useTranscriptContextMenu";
Expand Down Expand Up @@ -909,6 +910,7 @@ export const ChatPane: React.FC<ChatPaneProps> = (props) => {
</TooltipIfPresent>
</div>
)}
<AdditionalSystemContextChatDecoration workspaceId={workspaceId} />
{deferredMessages.map((msg, index) => {
const bashOutputGroup = bashOutputGroupInfos[index];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { useEffect, useRef, useState } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";

import { useAPI } from "@/browser/contexts/API";
import {
getAdditionalSystemContextVersion,
queueAdditionalSystemContextSave,
updateAdditionalSystemContextSnapshot,
useAdditionalSystemContextSnapshot,
} from "@/browser/utils/additionalSystemContextStore";
import { cn } from "@/common/lib/utils";
import { getErrorMessage } from "@/common/utils/errors";

function getFirstLinePreview(content: string): string {
return content.split(/\r?\n/, 1)[0]?.trim() || "(blank first line)";
}

interface ScratchpadState {
content: string;
loading: boolean;
saving: boolean;
error: string | null;
setContent: (content: string) => void;
}

export function useAdditionalSystemContextScratchpad(workspaceId: string): ScratchpadState {
const { api } = useAPI();
const content = useAdditionalSystemContextSnapshot(workspaceId);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const dirtyRef = useRef(false);
const mountedRef = useRef(true);

useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);

useEffect(() => {
dirtyRef.current = false;
setLoading(true);
setSaving(false);
setError(null);

if (!api) return;

const loadVersion = getAdditionalSystemContextVersion(workspaceId);
let cancelled = false;
api.workspace
.getAdditionalSystemContext({ workspaceId })
.then((result) => {
if (cancelled || !mountedRef.current) return;
if (!dirtyRef.current && getAdditionalSystemContextVersion(workspaceId) === loadVersion) {
updateAdditionalSystemContextSnapshot(workspaceId, result.content);
}
setLoading(false);
})
.catch((err) => {
if (cancelled || !mountedRef.current) return;
setError(getErrorMessage(err));
setLoading(false);
});

return () => {
cancelled = true;
};
}, [api, workspaceId]);

const setContent = (next: string) => {
dirtyRef.current = true;
updateAdditionalSystemContextSnapshot(workspaceId, next);
if (!api) return;
setSaving(true);
setError(null);
queueAdditionalSystemContextSave(api, workspaceId, next, {
onError: (err) => {
if (!mountedRef.current) return;
setError(getErrorMessage(err));
},
onIdle: () => {
if (!mountedRef.current) return;
setSaving(false);
},
});
};

return { content, loading, saving, error, setContent };
}

interface AdditionalSystemContextEditorProps {
workspaceId: string;
className?: string;
textareaClassName?: string;
minRows?: number;
placeholder?: string;
}

export function AdditionalSystemContextEditor(props: AdditionalSystemContextEditorProps) {
const state = useAdditionalSystemContextScratchpad(props.workspaceId);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);

useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
textarea.style.height = "0px";
textarea.style.height = `${textarea.scrollHeight}px`;
}, [state.content]);

return (
<div className={cn("space-y-1.5", props.className)}>
<textarea
ref={textareaRef}
value={state.content}
rows={props.minRows ?? 3}
onChange={(event) => state.setContent(event.currentTarget.value)}
placeholder={
props.placeholder ??
"Add workspace-specific context that should be appended to the system prompt…"
}
className={cn(
"border-border bg-background text-foreground placeholder:text-muted min-h-[72px] w-full resize-none overflow-hidden rounded border px-3 py-2 text-xs leading-5 outline-none focus:ring-1 focus:ring-[var(--color-accent)]",
props.textareaClassName
)}
aria-label="Additional system context scratchpad"
disabled={state.loading}
/>
<div className="text-muted flex min-h-4 items-center justify-between gap-2 text-[10px]">
<span>{state.loading ? "Loading…" : state.saving ? "Saving…" : "Saved automatically"}</span>
{state.error && <span className="text-destructive truncate">{state.error}</span>}
</div>
</div>
);
}

export function AdditionalSystemContextPanel(props: { workspaceId: string }) {
return (
<section className="border-border border-b px-3 py-3">
<div className="mb-2 flex items-baseline justify-between gap-3">
<div>
<h3 className="text-xs font-medium">Additional system context</h3>
<p className="text-muted mt-0.5 text-[10px]">
Scratchpad appended to the system prompt for every turn in this workspace.
</p>
</div>
</div>
<AdditionalSystemContextEditor workspaceId={props.workspaceId} />
</section>
);
}

export function AdditionalSystemContextChatDecoration(props: { workspaceId: string }) {
const state = useAdditionalSystemContextScratchpad(props.workspaceId);
const [expanded, setExpanded] = useState(false);
const hasContent = state.content.trim().length > 0;

if (state.loading || (!expanded && !hasContent)) {
return null;
}

return (
<div className="mx-auto w-full max-w-3xl px-4">
<div className="border-border bg-muted/10 rounded-lg border text-xs">
<button
type="button"
className="hover:bg-accent/20 flex w-full items-center gap-2 rounded-t-lg px-3 py-2 text-left transition-colors"
onClick={() => setExpanded((value) => !value)}
aria-expanded={expanded}
>
{expanded ? (
<ChevronDown className="text-muted h-3.5 w-3.5 shrink-0" />
) : (
<ChevronRight className="text-muted h-3.5 w-3.5 shrink-0" />
)}
<span className="font-medium">Additional system context</span>
{!expanded && (
<span className="text-muted min-w-0 truncate">
{getFirstLinePreview(state.content)}
</span>
)}
</button>
{expanded && (
<div className="border-border border-t p-3">
<AdditionalSystemContextEditor
workspaceId={props.workspaceId}
minRows={2}
textareaClassName="bg-background/80"
/>
</div>
)}
</div>
</div>
);
}
Loading
Loading