Remove problematic features: wake lock, artifacts panel, keyboard shortcuts, variable substitution

- Remove screen wake lock from chat-adapter.ts (marginal value, doesn't
  survive tab switches)
- Remove artifacts panel, artifact store, and ArtifactEmitter (session-scoped
  only, hash collision issues, duplicates existing code block actions)
- Remove keyboard shortcuts except Cmd/Ctrl+K for command palette (other
  shortcuts conflict with browser-native bindings on Linux/Windows)
- Remove keyboard-shortcut-help.tsx (no longer needed)
- Simplify prompt library to copy raw template content instead of using
  window.prompt() for variable substitution (blocking dialog, broken in
  some embedded contexts)
- Remove shortcut labels from command palette items
- Clean up unused imports

Kept: search, feedback, export, command palette (Cmd+K), memory, prompt
library (without variable substitution), folders/pinning.
This commit is contained in:
Daniel Han 2026-03-31 13:14:27 +00:00
commit 940faeaa73
7 changed files with 13 additions and 477 deletions

View file

@ -16,7 +16,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Block, type BlockProps, Streamdown } from "streamdown";
import "katex/dist/katex.min.css";
import { AudioPlayer } from "./audio-player";
import { useArtifactStore } from "@/features/chat/stores/artifact-store";
const math = createMathPlugin({ singleDollarTextMath: true });
const { withSmoothContextProvider } = INTERNAL;
@ -337,23 +336,6 @@ function CodeBlockActions({
);
}
/** Emits an artifact to the store via useEffect (avoids side-effects in render). */
function ArtifactEmitter({ language, source }: { language: string | null; source: string }) {
useEffect(() => {
const artifactId = `artifact-${hashCode(source)}`;
const store = useArtifactStore.getState();
if (store.artifacts.some((a) => a.id === artifactId)) return;
store.addArtifact({
id: artifactId,
title: language ? `${language} snippet` : "Code snippet",
language,
content: source,
createdAt: Date.now(),
});
}, [language, source]);
return null;
}
function StreamdownBlock(props: BlockProps) {
const hasMermaidFence = props.content.includes("```mermaid");
const mermaidSource = getMermaidSource(props.content);
@ -401,18 +383,8 @@ function StreamdownBlock(props: BlockProps) {
const svgSource = !props.isIncomplete && isSvgFence(codeFence) ? sanitizeSvg(codeFence.source) : null;
const htmlSource = !props.isIncomplete && isHtmlFence(codeFence) ? codeFence.source : null;
// Emit artifact for large code blocks or HTML/SVG/Mermaid
const lineCount = codeFence.source.split("\n").length;
const isArtifactWorthy =
!props.isIncomplete &&
(lineCount >= 20 || svgSource !== null || htmlSource !== null);
return (
<>
{isArtifactWorthy && (
<ArtifactEmitter language={codeFence.language} source={codeFence.source} />
)}
<div className="relative isolate">
<Block {...props} />
<CodeBlockActions

View file

@ -546,16 +546,6 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
return;
}
// Screen wake lock: keep screen on during long generations
let wakeLock: WakeLockSentinel | null = null;
try {
if ("wakeLock" in navigator) {
wakeLock = await navigator.wakeLock.request("screen");
}
} catch {
// Wake lock not available or denied -- continue without it
}
const threadKey = unstable_threadId || "__default";
let waitingFirstChunk = true;
let firstTokenSettled = false;
@ -809,11 +799,6 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
}
}
runtime.setThreadRunning(threadKey, false);
// Release screen wake lock
if (wakeLock) {
void wakeLock.release().catch(() => {});
wakeLock = null;
}
}
},
};

View file

@ -16,7 +16,6 @@ import {
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
} from "@/components/ui/command";
import { SidebarProvider, SidebarTrigger, useSidebar } from "@/components/ui/sidebar";
import {
@ -39,8 +38,6 @@ import {
BookOpenIcon,
BrainIcon,
ColumnsIcon,
KeyboardIcon,
PanelRightIcon,
PencilIcon,
SettingsIcon,
} from "lucide-react";
@ -56,9 +53,6 @@ import {
useState,
} from "react";
import { toast } from "sonner";
import { KeyboardShortcutHelp } from "./components/keyboard-shortcut-help";
import { ArtifactPanel } from "./components/artifact-panel";
import { useArtifactStore } from "./stores/artifact-store";
import { PromptLibrarySheet } from "./components/prompt-library-sheet";
import { GuidedTour, useGuidedTourController } from "@/features/tour";
import { listLocalModels } from "./api/chat-api";
@ -447,9 +441,7 @@ export function ChatPage(): ReactElement {
null,
);
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false);
const [promptLibraryOpen, setPromptLibraryOpen] = useState(false);
const artifactPanelOpen = useArtifactStore((s) => s.panelOpen);
const inferenceParams = useChatRuntimeStore((state) => state.params);
const setInferenceParams = useChatRuntimeStore((state) => state.setParams);
const activeGgufVariant = useChatRuntimeStore((state) => state.activeGgufVariant);
@ -530,14 +522,12 @@ export function ChatPage(): ReactElement {
const handleNewThread = useCallback(
() => {
useChatRuntimeStore.getState().setActiveThreadId(null);
useArtifactStore.getState().clearArtifacts();
setView({ mode: "single", newThreadNonce: crypto.randomUUID() });
},
[],
);
const handleNewCompare = useCallback(
() => {
useArtifactStore.getState().clearArtifacts();
setView({ mode: "compare", pairId: crypto.randomUUID() });
useChatRuntimeStore.getState().setContextUsage(null);
},
@ -566,7 +556,6 @@ export function ChatPage(): ReactElement {
const openSidebar = useCallback(() => setSidebarOpen(true), []);
const enterCompare = useCallback(() => {
useArtifactStore.getState().clearArtifacts();
setViewBeforeCompare((prev) => prev ?? view);
setView({ mode: "compare", pairId: crypto.randomUUID() });
useChatRuntimeStore.getState().setContextUsage(null);
@ -574,7 +563,6 @@ export function ChatPage(): ReactElement {
const exitCompare = useCallback(() => {
if (!viewBeforeCompare) return;
useArtifactStore.getState().clearArtifacts();
setView(viewBeforeCompare);
setViewBeforeCompare(null);
// Restore context usage from the active thread's last assistant message
@ -596,14 +584,7 @@ export function ChatPage(): ReactElement {
const handleThreadSelect = useCallback(
(nextView: ChatView) => {
setView((prev) => {
const prevId = prev.mode === "single" ? prev.threadId : prev.pairId;
const nextId = nextView.mode === "single" ? nextView.threadId : nextView.pairId;
if (prevId !== nextId) {
useArtifactStore.getState().clearArtifacts();
}
return nextView;
});
setView(nextView);
},
[],
);
@ -749,55 +730,22 @@ export function ChatPage(): ReactElement {
return () => window.clearTimeout(timeoutId);
}, [modelSelectorLocked, tour.open]);
// Global keyboard shortcuts
// Global keyboard shortcut: Cmd/Ctrl+K for command palette
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
const mod = e.metaKey || e.ctrlKey;
const target = e.target as HTMLElement;
const isInput =
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable;
if (mod && e.key === "k") {
e.preventDefault();
setCommandPaletteOpen((o) => !o);
return;
}
if (mod && e.shiftKey && e.key === "N") {
e.preventDefault();
handleNewThread();
return;
}
if (mod && e.shiftKey && e.key === "C") {
e.preventDefault();
if (canCompare) handleNewCompare();
return;
}
if (mod && e.shiftKey && e.key === "S") {
e.preventDefault();
setSettingsOpen((o) => !o);
return;
}
if (e.key === "Escape") {
setCommandPaletteOpen(false);
setShortcutHelpOpen(false);
return;
}
if (e.key === "?" && !isInput) {
e.preventDefault();
setShortcutHelpOpen((o) => !o);
return;
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [canCompare, handleNewThread, handleNewCompare]);
}, []);
return (
<div className="h-[calc(100dvh-4rem)] bg-background overflow-hidden">
<GuidedTour {...tour.tourProps} />
<KeyboardShortcutHelp open={shortcutHelpOpen} onOpenChange={setShortcutHelpOpen} />
<PromptLibrarySheet
open={promptLibraryOpen}
onOpenChange={setPromptLibraryOpen}
@ -815,19 +763,16 @@ export function ChatPage(): ReactElement {
<CommandItem onSelect={() => { handleNewThread(); setCommandPaletteOpen(false); }}>
<PencilIcon className="mr-2 size-4" />
New Chat
<CommandShortcut>Shift+N</CommandShortcut>
</CommandItem>
{canCompare && (
<CommandItem onSelect={() => { handleNewCompare(); setCommandPaletteOpen(false); }}>
<ColumnsIcon className="mr-2 size-4" />
Compare Mode
<CommandShortcut>Shift+C</CommandShortcut>
</CommandItem>
)}
<CommandItem onSelect={() => { setSettingsOpen(true); setCommandPaletteOpen(false); }}>
<SettingsIcon className="mr-2 size-4" />
Settings
<CommandShortcut>Shift+S</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Actions">
@ -835,22 +780,11 @@ export function ChatPage(): ReactElement {
<BookOpenIcon className="mr-2 size-4" />
Prompt Library
</CommandItem>
<CommandItem onSelect={() => { const s = useArtifactStore.getState(); s.setPanelOpen(!s.panelOpen); setCommandPaletteOpen(false); }}>
<PanelRightIcon className="mr-2 size-4" />
Toggle Artifacts Panel
</CommandItem>
<CommandItem onSelect={() => { handleEject(); setCommandPaletteOpen(false); }}>
<BrainIcon className="mr-2 size-4" />
Eject Model
</CommandItem>
</CommandGroup>
<CommandGroup heading="Help">
<CommandItem onSelect={() => { setShortcutHelpOpen(true); setCommandPaletteOpen(false); }}>
<KeyboardIcon className="mr-2 size-4" />
Keyboard Shortcuts
<CommandShortcut>?</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</CommandDialog>
@ -943,22 +877,15 @@ export function ChatPage(): ReactElement {
</button>
</div>
<div className="flex min-h-0 flex-1">
<div className="min-h-0 min-w-0 flex-1">
{view.mode === "single" ? (
<SingleContent
key={view.threadId ?? view.newThreadNonce ?? "new"}
threadId={view.threadId}
newThreadNonce={view.newThreadNonce}
/>
) : (
<CompareContent key={view.pairId} pairId={view.pairId} models={models} loraModels={loraModels} />
)}
</div>
{artifactPanelOpen && (
<div className="hidden w-[24rem] shrink-0 md:block">
<ArtifactPanel />
</div>
<div className="min-h-0 flex-1">
{view.mode === "single" ? (
<SingleContent
key={view.threadId ?? view.newThreadNonce ?? "new"}
threadId={view.threadId}
newThreadNonce={view.newThreadNonce}
/>
) : (
<CompareContent key={view.pairId} pairId={view.pairId} models={models} loraModels={loraModels} />
)}
</div>
</div>

View file

@ -1,187 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { Button } from "@/components/ui/button";
import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { downloadTextFile } from "@/lib/download";
import {
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
DownloadIcon,
XIcon,
} from "lucide-react";
import { type FC, useEffect, useRef, useState } from "react";
import {
type Artifact,
useArtifactStore,
} from "../stores/artifact-store";
const COPY_RESET_MS = 2000;
const ArtifactTab: FC<{ artifact: Artifact; isActive: boolean }> = ({
artifact,
isActive,
}) => {
const setActive = useArtifactStore((s) => s.setActiveArtifact);
const remove = useArtifactStore((s) => s.removeArtifact);
return (
<div
role="tab"
tabIndex={0}
onClick={() => setActive(artifact.id)}
onKeyDown={(e) => e.key === "Enter" && setActive(artifact.id)}
className={`group flex cursor-pointer items-center gap-1.5 rounded-t-md border-b-2 px-3 py-1.5 text-xs font-medium transition-colors ${
isActive
? "border-primary bg-background text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<span className="max-w-24 truncate">{artifact.title}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
remove(artifact.id);
}}
className="size-4 rounded opacity-0 hover:bg-destructive/20 group-hover:opacity-100"
>
<XIcon className="size-3" />
</button>
</div>
);
};
export const ArtifactPanel: FC = () => {
const artifacts = useArtifactStore((s) => s.artifacts);
const activeId = useArtifactStore((s) => s.activeArtifactId);
const panelOpen = useArtifactStore((s) => s.panelOpen);
const setPanelOpen = useArtifactStore((s) => s.setPanelOpen);
const setVersion = useArtifactStore((s) => s.setActiveVersion);
const updateContent = useArtifactStore((s) => s.updateArtifactContent);
const [copied, setCopied] = useState(false);
const [localValue, setLocalValue] = useState("");
const resetRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const active = artifacts.find((a) => a.id === activeId) ?? artifacts[0];
const viewedContent = active
? (active.history[active.activeVersion] ?? active.content)
: "";
// Sync local editor value when switching artifacts or versions
useEffect(() => {
if (!active) return;
setLocalValue(active.history[active.activeVersion] ?? active.content);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active?.id, active?.activeVersion]);
// Cleanup copy timer on unmount
useEffect(() => {
return () => {
if (resetRef.current) clearTimeout(resetRef.current);
};
}, []);
if (!panelOpen || artifacts.length === 0 || !active) return null;
const handleCopy = () => {
if (copyToClipboard(localValue)) {
setCopied(true);
if (resetRef.current) clearTimeout(resetRef.current);
resetRef.current = setTimeout(() => setCopied(false), COPY_RESET_MS);
}
};
const handleDownload = () => {
const ext = active.language === "html"
? ".html"
: active.language === "svg"
? ".svg"
: active.language
? `.${active.language}`
: ".txt";
downloadTextFile(`${active.title}${ext}`, localValue);
};
const canPrev = active.activeVersion > 0;
const canNext = active.activeVersion < active.history.length - 1;
return (
<div className="flex h-full flex-col border-l bg-background">
{/* Tab bar */}
<div className="flex shrink-0 items-center border-b px-2">
<div className="flex flex-1 items-center gap-0.5 overflow-x-auto">
{artifacts.map((a) => (
<ArtifactTab
key={a.id}
artifact={a}
isActive={a.id === active.id}
/>
))}
</div>
<Button
variant="ghost"
size="icon"
className="size-7 shrink-0"
onClick={() => setPanelOpen(false)}
>
<XIcon className="size-4" />
</Button>
</div>
{/* Toolbar */}
<div className="flex shrink-0 items-center gap-1 border-b px-3 py-1.5">
<span className="flex-1 truncate text-xs font-medium">{active.title}</span>
{active.history.length > 1 && (
<div className="flex items-center gap-0.5 text-xs text-muted-foreground">
<button
type="button"
disabled={!canPrev}
onClick={() => setVersion(active.id, active.activeVersion - 1)}
className="p-0.5 disabled:opacity-30"
>
<ChevronLeftIcon className="size-3.5" />
</button>
<span className="tabular-nums">
v{active.activeVersion + 1}/{active.history.length}
</span>
<button
type="button"
disabled={!canNext}
onClick={() => setVersion(active.id, active.activeVersion + 1)}
className="p-0.5 disabled:opacity-30"
>
<ChevronRightIcon className="size-3.5" />
</button>
</div>
)}
<button type="button" onClick={handleCopy} className="p-1 text-muted-foreground hover:text-foreground">
{copied ? <CheckIcon className="size-3.5" /> : <CopyIcon className="size-3.5" />}
</button>
<button type="button" onClick={handleDownload} className="p-1 text-muted-foreground hover:text-foreground">
<DownloadIcon className="size-3.5" />
</button>
</div>
{/* Editor */}
<div className="flex-1 overflow-auto">
<textarea
ref={textareaRef}
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
onBlur={() => {
if (localValue !== viewedContent) {
updateContent(active.id, localValue);
}
}}
className="h-full w-full resize-none bg-transparent p-4 font-mono text-xs leading-relaxed outline-none"
spellCheck={false}
/>
</div>
</div>
);
};

View file

@ -1,52 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import type { FC } from "react";
const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
const mod = isMac ? "\u2318" : "Ctrl";
const shortcuts = [
{ keys: `${mod}+K`, description: "Open command palette" },
{ keys: `${mod}+Shift+N`, description: "New chat" },
{ keys: `${mod}+Shift+C`, description: "Toggle compare mode" },
{ keys: `${mod}+Shift+S`, description: "Toggle settings" },
{ keys: "Escape", description: "Close dialogs / Cancel" },
{ keys: "?", description: "Show this help" },
];
export const KeyboardShortcutHelp: FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Keyboard Shortcuts</DialogTitle>
<DialogDescription>Quick actions for power users</DialogDescription>
</DialogHeader>
<div className="space-y-1">
{shortcuts.map(({ keys, description }) => (
<div
key={keys}
className="flex items-center justify-between py-1.5 text-sm"
>
<span className="text-muted-foreground">{description}</span>
<kbd className="rounded border bg-muted px-2 py-0.5 font-mono text-xs">
{keys}
</kbd>
</div>
))}
</div>
</DialogContent>
</Dialog>
);
};

View file

@ -81,15 +81,7 @@ export const PromptLibrarySheet: FC<{
const handleInsert = useCallback(
(p: PromptRecord) => {
let result = p.content;
if (p.variables.length > 0) {
for (const v of p.variables) {
const value = window.prompt(`Value for {{${v}}}:`);
if (value === null) return; // user cancelled
result = result.split(`{{${v}}}`).join(value);
}
}
onInsertPrompt?.(result);
onInsertPrompt?.(p.content);
onOpenChange(false);
},
[onInsertPrompt, onOpenChange],

View file

@ -1,101 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { create } from "zustand";
export interface Artifact {
id: string;
title: string;
language: string | null;
content: string;
/** Version history, most recent last */
history: string[];
/** Index into history array for current view */
activeVersion: number;
createdAt: number;
}
interface ArtifactStore {
artifacts: Artifact[];
activeArtifactId: string | null;
panelOpen: boolean;
addArtifact: (artifact: Omit<Artifact, "history" | "activeVersion">) => void;
updateArtifactContent: (id: string, content: string) => void;
setActiveArtifact: (id: string | null) => void;
setActiveVersion: (id: string, version: number) => void;
removeArtifact: (id: string) => void;
setPanelOpen: (open: boolean) => void;
clearArtifacts: () => void;
}
export const useArtifactStore = create<ArtifactStore>((set, get) => ({
artifacts: [],
activeArtifactId: null,
panelOpen: false,
addArtifact: (artifact) => {
const existing = get().artifacts.find((a) => a.id === artifact.id);
if (existing) return;
set((state) => ({
artifacts: [
...state.artifacts,
{
...artifact,
history: [artifact.content],
activeVersion: 0,
},
],
activeArtifactId: artifact.id,
panelOpen: true,
}));
},
updateArtifactContent: (id, content) => {
set((state) => ({
artifacts: state.artifacts.map((a) =>
a.id === id
? {
...a,
content,
history: [...a.history, content],
activeVersion: a.history.length,
}
: a,
),
}));
},
setActiveArtifact: (id) => {
set({ activeArtifactId: id, panelOpen: id !== null });
},
setActiveVersion: (id, version) => {
set((state) => ({
artifacts: state.artifacts.map((a) =>
a.id === id
? { ...a, activeVersion: version, content: a.history[version] ?? a.content }
: a,
),
}));
},
removeArtifact: (id) => {
set((state) => {
const filtered = state.artifacts.filter((a) => a.id !== id);
return {
artifacts: filtered,
activeArtifactId:
state.activeArtifactId === id
? filtered[0]?.id ?? null
: state.activeArtifactId,
panelOpen: filtered.length > 0,
};
});
},
setPanelOpen: (open) => set({ panelOpen: open }),
clearArtifacts: () =>
set({ artifacts: [], activeArtifactId: null, panelOpen: false }),
}));