-
Notifications
You must be signed in to change notification settings - Fork 105
🤖 feat: Instructions tab in right sidebar #3262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 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 598ca40
🤖 refactor: unify right-sidebar tab registry; add Instructions to def…
ammar-agent d881d1c
🤖 fix: parent AGENTS.md for sub-project workspaces; align Instruction…
ammar-agent 00a958e
🤖 fix: keep Instructions tab default-visible and unblock CI
ammar-agent 07501b1
🤖 feat: workspace additional system context scratchpad
ammar-agent d9c68b8
🤖 fix: mock additional system context in Storybook
ammar-agent 8b2b643
🤖 fix: use live scratchpad context for sends
ammar-agent 0e9b898
🤖 fix: isolate scratchpad save generations
ammar-agent f42cdaa
🤖 fix: send live scratchpad separately
ammar-agent 38e270e
🤖 fix: keep scratchpad save queue across unmounts
ammar-agent 02ad108
🤖 fix: hydrate scratchpad before live override
ammar-agent 2abd728
🤖 fix: ignore stale scratchpad save responses
ammar-agent 24f6b9a
🤖 tests: update sidebar archive layout expectation
ammar-agent 7825079
🤖 tests: account for Instructions tab in sidebar e2e
ammar-agent 5a60c99
🤖 feat: count badge for Instructions tab label
ammar-agent ce8eadf
🤖 feat: Chat Instructions toggle + chat decoration + delayed save flash
ammar-agent 344b36d
🤖 refactor: Chat Instructions decoration as link + orange dirty asterisk
ammar-agent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
250 changes: 250 additions & 0 deletions
250
src/browser/components/InstructionsTab/AdditionalSystemContextScratchpad.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { ChevronDown, ChevronRight } from "lucide-react"; | ||
|
|
||
| import { useAPI } from "@/browser/contexts/API"; | ||
| import { cn } from "@/common/lib/utils"; | ||
| import { getErrorMessage } from "@/common/utils/errors"; | ||
|
|
||
| const ADDITIONAL_SYSTEM_CONTEXT_EVENT = "mux:additional-system-context-changed"; | ||
|
|
||
| interface AdditionalSystemContextEventDetail { | ||
| workspaceId: string; | ||
| content: string; | ||
| } | ||
|
|
||
| function emitAdditionalSystemContextChanged(workspaceId: string, content: string): void { | ||
| if (typeof window === "undefined") return; | ||
| window.dispatchEvent( | ||
| new CustomEvent<AdditionalSystemContextEventDetail>(ADDITIONAL_SYSTEM_CONTEXT_EVENT, { | ||
| detail: { workspaceId, content }, | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| function getFirstLinePreview(content: string): string { | ||
| return content.split(/\r?\n/, 1)[0]?.trim() || "(blank first line)"; | ||
| } | ||
|
|
||
| function isAdditionalSystemContextEvent( | ||
| event: Event | ||
| ): event is CustomEvent<AdditionalSystemContextEventDetail> { | ||
| const detail = (event as CustomEvent<AdditionalSystemContextEventDetail>).detail; | ||
| return ( | ||
| detail != null && typeof detail.workspaceId === "string" && typeof detail.content === "string" | ||
| ); | ||
| } | ||
|
|
||
| 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, setContentState] = useState(""); | ||
| const [loading, setLoading] = useState(true); | ||
| const [saving, setSaving] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const dirtyRef = useRef(false); | ||
| const inFlightSaveRef = useRef(false); | ||
| const pendingSaveRef = useRef<string | null>(null); | ||
| const mountedRef = useRef(true); | ||
|
|
||
| useEffect(() => { | ||
| mountedRef.current = true; | ||
| return () => { | ||
| mountedRef.current = false; | ||
| }; | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| dirtyRef.current = false; | ||
| pendingSaveRef.current = null; | ||
| inFlightSaveRef.current = false; | ||
| setContentState(""); | ||
| setLoading(true); | ||
| setSaving(false); | ||
| setError(null); | ||
|
|
||
| if (!api) return; | ||
|
|
||
| let cancelled = false; | ||
| api.workspace | ||
| .getAdditionalSystemContext({ workspaceId }) | ||
| .then((result) => { | ||
| if (cancelled || !mountedRef.current) return; | ||
| if (!dirtyRef.current) { | ||
| setContentState(result.content); | ||
| emitAdditionalSystemContextChanged(workspaceId, result.content); | ||
| } | ||
| setLoading(false); | ||
| }) | ||
| .catch((err) => { | ||
| if (cancelled || !mountedRef.current) return; | ||
| setError(getErrorMessage(err)); | ||
| setLoading(false); | ||
| }); | ||
|
|
||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [api, workspaceId]); | ||
|
|
||
| useEffect(() => { | ||
| const handleChanged = (event: Event) => { | ||
| if (!isAdditionalSystemContextEvent(event)) return; | ||
| if (event.detail.workspaceId !== workspaceId) return; | ||
| setContentState(event.detail.content); | ||
| }; | ||
| window.addEventListener(ADDITIONAL_SYSTEM_CONTEXT_EVENT, handleChanged); | ||
| return () => window.removeEventListener(ADDITIONAL_SYSTEM_CONTEXT_EVENT, handleChanged); | ||
| }, [workspaceId]); | ||
|
|
||
| const flushSave = () => { | ||
| if (!api || inFlightSaveRef.current) return; | ||
| const next = pendingSaveRef.current; | ||
| if (next == null) return; | ||
|
|
||
| pendingSaveRef.current = null; | ||
| inFlightSaveRef.current = true; | ||
| setSaving(true); | ||
| setError(null); | ||
|
|
||
| api.workspace | ||
| .setAdditionalSystemContext({ workspaceId, content: next }) | ||
| .then((result) => { | ||
| if (!mountedRef.current) return; | ||
| emitAdditionalSystemContextChanged(workspaceId, result.content); | ||
| }) | ||
| .catch((err) => { | ||
| if (!mountedRef.current) return; | ||
| setError(getErrorMessage(err)); | ||
| }) | ||
| .finally(() => { | ||
| if (!mountedRef.current) return; | ||
| inFlightSaveRef.current = false; | ||
| if (pendingSaveRef.current == null) { | ||
| setSaving(false); | ||
| } | ||
| flushSave(); | ||
|
ammar-agent marked this conversation as resolved.
Outdated
|
||
| }); | ||
| }; | ||
|
|
||
| const setContent = (next: string) => { | ||
| dirtyRef.current = true; | ||
| setContentState(next); | ||
| emitAdditionalSystemContextChanged(workspaceId, next); | ||
| pendingSaveRef.current = next; | ||
| flushSave(); | ||
|
ammar-agent marked this conversation as resolved.
Outdated
|
||
| }; | ||
|
|
||
| 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> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.