feat(quick-pane): add chat-style exchange
This commit is contained in:
+327
-135
@@ -2,6 +2,8 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { ExtensionDialog } from "../components/ExtensionDialog";
|
||||
import {
|
||||
catalogModels,
|
||||
@@ -20,6 +22,8 @@ import type {
|
||||
UiState,
|
||||
} from "../workspace/types";
|
||||
|
||||
const ESCALATION_MARKER = "[ESCALATE]";
|
||||
|
||||
const defaults: QuickPaneSettings = {
|
||||
workspace: "",
|
||||
provider: "",
|
||||
@@ -34,6 +38,10 @@ const defaults: QuickPaneSettings = {
|
||||
|
||||
type Runtime = { agentId: string; runtimeId: string };
|
||||
type HistoryEntry = { id: string; request: string; answer: string };
|
||||
type ActivitySnapshot = {
|
||||
activeTool?: string;
|
||||
hasAssistantText: boolean;
|
||||
};
|
||||
|
||||
function historyLabel(request: string) {
|
||||
const concise = request.replace(/\s+/g, " ").trim();
|
||||
@@ -76,10 +84,29 @@ function assistantText(snapshot: RuntimeSnapshot) {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function toolActivityLabel(tool: string) {
|
||||
const segments = tool.split(".");
|
||||
const name = segments[segments.length - 1]?.replace(/[_-]+/g, " ") ?? tool;
|
||||
const labels: Record<string, string> = {
|
||||
bash: "Running command",
|
||||
edit: "Editing file",
|
||||
find: "Finding files",
|
||||
grep: "Searching files",
|
||||
ls: "Listing files",
|
||||
read: "Reading file",
|
||||
write: "Writing file",
|
||||
};
|
||||
return (
|
||||
labels[name.toLowerCase()] ??
|
||||
`${name.charAt(0).toUpperCase()}${name.slice(1)}`
|
||||
);
|
||||
}
|
||||
|
||||
export default function QuickPane() {
|
||||
const [settings, setSettings] = useState<QuickPaneSettings>(defaults);
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||
const [request, setRequest] = useState("");
|
||||
const [currentRequest, setCurrentRequest] = useState("");
|
||||
const [submissionStarting, setSubmissionStarting] = useState(false);
|
||||
const [thinkingLevel, setThinkingLevel] = useState("medium");
|
||||
const [models, setModels] = useState<Model[]>([]);
|
||||
@@ -87,22 +114,29 @@ export default function QuickPane() {
|
||||
provider: "",
|
||||
modelId: "",
|
||||
});
|
||||
const [status, setStatus] = useState("Loading Quick Pane settings…");
|
||||
const [notice, setNotice] = useState("Loading Quick Pane settings…");
|
||||
const [error, setError] = useState<string>();
|
||||
const [catalogError, setCatalogError] = useState<string>();
|
||||
const [runtime, setRuntime] = useState<Runtime>();
|
||||
const [answer, setAnswer] = useState("");
|
||||
const [completedRuntimeId, setCompletedRuntimeId] = useState<string>();
|
||||
const [failedRuntimeId, setFailedRuntimeId] = useState<string>();
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState("");
|
||||
const [escalation, setEscalation] = useState<string>();
|
||||
const [extension, setExtension] = useState<Extension>();
|
||||
const [activitySnapshot, setActivitySnapshot] = useState<ActivitySnapshot>({
|
||||
hasAssistantText: false,
|
||||
});
|
||||
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
||||
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
||||
const submittedRequestRef = useRef<
|
||||
{ runtimeId: string; request: string } | undefined
|
||||
>(undefined);
|
||||
const promptSubmittedRuntimeRef = useRef<string | undefined>(undefined);
|
||||
const completedRuntimeRef = useRef<string | undefined>(undefined);
|
||||
const submissionStartingRef = useRef(false);
|
||||
const runStartedAtRef = useRef<number | undefined>(undefined);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const invalid = useMemo(() => settingsError(settings), [settings]);
|
||||
const modelOptions = useMemo(
|
||||
@@ -113,11 +147,32 @@ export default function QuickPane() {
|
||||
selectedModel.provider && selectedModel.modelId
|
||||
? modelIdentityValue(selectedModel)
|
||||
: "";
|
||||
const canContinue = !!escalation || (!!runtime && !!error);
|
||||
const displayedAnswer = selectedHistoryId
|
||||
? (history.find((entry) => entry.id === selectedHistoryId)?.answer ??
|
||||
answer)
|
||||
: answer;
|
||||
const runActive =
|
||||
submissionStarting ||
|
||||
(!!runtime &&
|
||||
completedRuntimeId !== runtime.runtimeId &&
|
||||
failedRuntimeId !== runtime.runtimeId);
|
||||
const viewingHistory = !!selectedHistoryId && !runActive;
|
||||
const displayedEntry = viewingHistory
|
||||
? history.find((entry) => entry.id === selectedHistoryId)
|
||||
: undefined;
|
||||
const displayedRequest = displayedEntry?.request ?? currentRequest;
|
||||
const displayedAnswer = displayedEntry?.answer ?? answer;
|
||||
const displayedAnswerComplete =
|
||||
!viewingHistory && !!runtime && completedRuntimeId === runtime.runtimeId;
|
||||
const activityPhase = submissionStarting
|
||||
? "Starting"
|
||||
: runActive && activitySnapshot.activeTool
|
||||
? toolActivityLabel(activitySnapshot.activeTool)
|
||||
: runActive && activitySnapshot.hasAssistantText
|
||||
? "Answering"
|
||||
: runActive
|
||||
? "Thinking"
|
||||
: "";
|
||||
const activity =
|
||||
activityPhase === "Thinking"
|
||||
? `${activityPhase} · ${elapsedSeconds}s`
|
||||
: activityPhase;
|
||||
|
||||
const dismissPane = () => getCurrentWindow().hide();
|
||||
const focusInput = () => inputRef.current?.focus();
|
||||
@@ -149,7 +204,7 @@ export default function QuickPane() {
|
||||
provider: nextSettings.provider,
|
||||
modelId: nextSettings.model,
|
||||
});
|
||||
setStatus("");
|
||||
setNotice("");
|
||||
if (active) await loadModelCatalog();
|
||||
})();
|
||||
return () => {
|
||||
@@ -166,6 +221,22 @@ export default function QuickPane() {
|
||||
if (settingsLoaded) focusInput();
|
||||
}, [settingsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!runActive) return;
|
||||
const updateElapsed = () =>
|
||||
setElapsedSeconds(
|
||||
Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
(Date.now() - (runStartedAtRef.current ?? Date.now())) / 1000,
|
||||
),
|
||||
),
|
||||
);
|
||||
updateElapsed();
|
||||
const interval = window.setInterval(updateElapsed, 1_000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [runActive]);
|
||||
|
||||
useEffect(() => {
|
||||
const paneWindow = getCurrentWindow();
|
||||
const settingsListener = listen<QuickPaneSettings>(
|
||||
@@ -209,42 +280,58 @@ export default function QuickPane() {
|
||||
);
|
||||
if (!active) return;
|
||||
const text = assistantText(snapshot);
|
||||
if (text) {
|
||||
setAnswer(text);
|
||||
if (text.includes("[ESCALATE]")) {
|
||||
setEscalation(
|
||||
text.replace("[ESCALATE]", "").trim() ||
|
||||
"Quick agent requested a full session.",
|
||||
const responseComplete =
|
||||
snapshot.state?.data?.isStreaming === false &&
|
||||
promptSubmittedRuntimeRef.current === runtime.runtimeId;
|
||||
const escalated = text.startsWith(ESCALATION_MARKER);
|
||||
const awaitingEscalationMarker =
|
||||
!!text && ESCALATION_MARKER.startsWith(text);
|
||||
const result = awaitingEscalationMarker
|
||||
? ""
|
||||
: escalated
|
||||
? text.slice(ESCALATION_MARKER.length).trim()
|
||||
: text;
|
||||
setActivitySnapshot({
|
||||
activeTool: snapshot.runtime.activeTool,
|
||||
hasAssistantText: !!text,
|
||||
});
|
||||
if (awaitingEscalationMarker) {
|
||||
setAnswer("");
|
||||
} else if (text) {
|
||||
setAnswer(result);
|
||||
}
|
||||
if (escalated && responseComplete) {
|
||||
setEscalation(result || "Quick agent requested a full session.");
|
||||
}
|
||||
if (
|
||||
responseComplete &&
|
||||
completedRuntimeRef.current !== runtime.runtimeId
|
||||
) {
|
||||
completedRuntimeRef.current = runtime.runtimeId;
|
||||
setCompletedRuntimeId(runtime.runtimeId);
|
||||
setNotice("");
|
||||
const submitted = submittedRequestRef.current;
|
||||
if (submitted?.runtimeId === runtime.runtimeId) {
|
||||
setRequest((current) =>
|
||||
current === submitted.request ? "" : current,
|
||||
);
|
||||
setHistory((current) =>
|
||||
[
|
||||
{
|
||||
id: runtime.runtimeId,
|
||||
request: submitted.request,
|
||||
answer: result,
|
||||
},
|
||||
...current.filter((entry) => entry.id !== runtime.runtimeId),
|
||||
].slice(0, 5),
|
||||
);
|
||||
}
|
||||
if (
|
||||
snapshot.state?.data?.isStreaming === false &&
|
||||
completedRuntimeRef.current !== runtime.runtimeId
|
||||
) {
|
||||
completedRuntimeRef.current = runtime.runtimeId;
|
||||
setCompletedRuntimeId(runtime.runtimeId);
|
||||
setStatus("Quick answer ready.");
|
||||
const submitted = submittedRequestRef.current;
|
||||
if (submitted?.runtimeId === runtime.runtimeId) {
|
||||
setRequest((current) =>
|
||||
current === submitted.request ? "" : current,
|
||||
);
|
||||
setHistory((current) =>
|
||||
[
|
||||
{
|
||||
id: runtime.runtimeId,
|
||||
request: submitted.request,
|
||||
answer: text,
|
||||
},
|
||||
...current.filter((entry) => entry.id !== runtime.runtimeId),
|
||||
].slice(0, 5),
|
||||
);
|
||||
}
|
||||
}
|
||||
window.requestAnimationFrame(focusInput);
|
||||
}
|
||||
setExtension(snapshot.extensions[0]);
|
||||
if (snapshot.runtime.error?.message) {
|
||||
setError(snapshot.runtime.error.message);
|
||||
setFailedRuntimeId(runtime.runtimeId);
|
||||
setEscalation(
|
||||
"Quick runtime failed. Continue in a full session if needed.",
|
||||
);
|
||||
@@ -268,21 +355,23 @@ export default function QuickPane() {
|
||||
setEscalation(
|
||||
`Quick time budget of ${settings.maximumSeconds} seconds reached.`,
|
||||
);
|
||||
setStatus(
|
||||
"Quick time budget reached. Showing partial result if available.",
|
||||
);
|
||||
setFailedRuntimeId(runtime.runtimeId);
|
||||
invoke("abort", { agentId: runtime.agentId }).catch(() => undefined);
|
||||
}, settings.maximumSeconds * 1000);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [completedRuntimeId, runtime, settings.maximumSeconds]);
|
||||
|
||||
async function run() {
|
||||
if (submissionStartingRef.current) return;
|
||||
if (submissionStartingRef.current || runActive) return;
|
||||
if (invalid || !request.trim())
|
||||
return setError(invalid ?? "Enter a request.");
|
||||
submissionStartingRef.current = true;
|
||||
setSubmissionStarting(true);
|
||||
const submittedRequest = request;
|
||||
setCurrentRequest(submittedRequest);
|
||||
setSelectedHistoryId("");
|
||||
runStartedAtRef.current = Date.now();
|
||||
setElapsedSeconds(0);
|
||||
try {
|
||||
const previous = runtimeRef.current;
|
||||
if (previous)
|
||||
@@ -291,15 +380,17 @@ export default function QuickPane() {
|
||||
}).catch(() => undefined);
|
||||
runtimeRef.current = undefined;
|
||||
submittedRequestRef.current = undefined;
|
||||
promptSubmittedRuntimeRef.current = undefined;
|
||||
completedRuntimeRef.current = undefined;
|
||||
setRuntime(undefined);
|
||||
setCompletedRuntimeId(undefined);
|
||||
setSelectedHistoryId("");
|
||||
setFailedRuntimeId(undefined);
|
||||
setError(undefined);
|
||||
setAnswer("");
|
||||
setEscalation(undefined);
|
||||
setExtension(undefined);
|
||||
setStatus("Starting advisory quick agent…");
|
||||
setActivitySnapshot({ hasAssistantText: false });
|
||||
setNotice("");
|
||||
const result = await invoke<{ runtime: Runtime }>(
|
||||
"create_quick_runtime",
|
||||
{
|
||||
@@ -326,17 +417,15 @@ export default function QuickPane() {
|
||||
agentId: result.runtime.agentId,
|
||||
message: guardrails,
|
||||
});
|
||||
if (completedRuntimeRef.current !== result.runtime.runtimeId) {
|
||||
setStatus(
|
||||
"Quick request running. Advisory limits are not trusted enforcement.",
|
||||
);
|
||||
}
|
||||
promptSubmittedRuntimeRef.current = result.runtime.runtimeId;
|
||||
} catch (reason) {
|
||||
setError(`Could not start quick request: ${String(reason)}`);
|
||||
setStatus("");
|
||||
setFailedRuntimeId(runtimeRef.current?.runtimeId);
|
||||
} finally {
|
||||
submissionStartingRef.current = false;
|
||||
setSubmissionStarting(false);
|
||||
focusInput();
|
||||
window.requestAnimationFrame(focusInput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +445,7 @@ export default function QuickPane() {
|
||||
|
||||
async function continueFull() {
|
||||
const handoffRequest =
|
||||
request.trim() || submittedRequestRef.current?.request || "";
|
||||
request.trim() || submittedRequestRef.current?.request || currentRequest;
|
||||
if (invalid || !handoffRequest)
|
||||
return setError(invalid ?? "Enter a request.");
|
||||
try {
|
||||
@@ -375,7 +464,7 @@ export default function QuickPane() {
|
||||
agentId: result.runtime.agentId,
|
||||
message: `${handoffRequest}\n\nQuick Pane escalation: ${escalation ?? "Continue this in a full session."}\n\nPartial quick result:\n${answer}`,
|
||||
});
|
||||
setStatus("Full session started with follow-up model.");
|
||||
setNotice("Full session started with follow-up model.");
|
||||
} catch (reason) {
|
||||
setError(`Could not start full session: ${String(reason)}`);
|
||||
}
|
||||
@@ -383,20 +472,74 @@ export default function QuickPane() {
|
||||
|
||||
return (
|
||||
<main className="quick-pane">
|
||||
<header>
|
||||
<strong>Quick Pane</strong>
|
||||
<div className="quick-pane-header-controls">
|
||||
<header className="quick-pane-toolbar">
|
||||
<div className="quick-pane-primary-controls">
|
||||
<label className="quick-pane-field quick-pane-model">
|
||||
<span className="full-label">Model</span>
|
||||
<span className="compact-label" aria-hidden="true">
|
||||
M
|
||||
</span>
|
||||
<select
|
||||
aria-label="Model"
|
||||
value={selectedModelValue}
|
||||
disabled={runActive}
|
||||
onChange={(event) => {
|
||||
const identity = modelIdentityFromValue(
|
||||
event.currentTarget.value,
|
||||
);
|
||||
if (identity) setSelectedModel(identity);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select model
|
||||
</option>
|
||||
{modelOptions.map((model) => {
|
||||
const identity = modelIdentity(model);
|
||||
if (!identity) return null;
|
||||
return (
|
||||
<option
|
||||
key={modelIdentityValue(identity)}
|
||||
value={modelIdentityValue(identity)}
|
||||
>
|
||||
{identity.provider}/{identity.modelId}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
<label className="quick-pane-field quick-pane-thinking">
|
||||
<span className="full-label">Thinking</span>
|
||||
<span className="compact-label" aria-hidden="true">
|
||||
T
|
||||
</span>
|
||||
<select
|
||||
aria-label="Thinking"
|
||||
value={thinkingLevel}
|
||||
disabled={runActive}
|
||||
onChange={(event) => setThinkingLevel(event.currentTarget.value)}
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="quick-pane-header-actions">
|
||||
{history.length > 0 && (
|
||||
<label className="quick-pane-history">
|
||||
<span>History</span>
|
||||
<label className="quick-pane-field quick-pane-history">
|
||||
<span className="full-label">History</span>
|
||||
<span className="compact-label" aria-hidden="true">
|
||||
H
|
||||
</span>
|
||||
<select
|
||||
aria-label="Answer history"
|
||||
value={selectedHistoryId}
|
||||
value={viewingHistory ? selectedHistoryId : ""}
|
||||
disabled={runActive}
|
||||
onChange={(event) =>
|
||||
setSelectedHistoryId(event.currentTarget.value)
|
||||
}
|
||||
>
|
||||
<option value="">Current answer</option>
|
||||
<option value="">Current</option>
|
||||
{history.map((entry) => (
|
||||
<option key={entry.id} value={entry.id}>
|
||||
{historyLabel(entry.request)}
|
||||
@@ -405,93 +548,142 @@ export default function QuickPane() {
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<button onClick={dismissPane}>Close</button>
|
||||
<button
|
||||
className="quick-pane-close"
|
||||
type="button"
|
||||
aria-label="Close Quick Pane"
|
||||
title="Close Quick Pane (Esc)"
|
||||
onClick={dismissPane}
|
||||
>
|
||||
<span className="full-label">Close</span>
|
||||
<span className="compact-label" aria-hidden="true">
|
||||
×
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<textarea
|
||||
className="quick-pane-request"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={request}
|
||||
onChange={(event) => setRequest(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
event.preventDefault();
|
||||
void run();
|
||||
}
|
||||
}}
|
||||
placeholder="Ask a quick question or give a quick instruction"
|
||||
/>
|
||||
<section className="quick-pane-output" aria-live="polite">
|
||||
{status && <p>{status}</p>}
|
||||
{displayedAnswer && <pre>{displayedAnswer}</pre>}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{catalogError && <p role="alert">{catalogError}</p>}
|
||||
|
||||
<section className="quick-pane-exchange" aria-label="Quick exchange">
|
||||
{displayedRequest && (
|
||||
<div className="quick-pane-user-row">
|
||||
<article className="quick-pane-user-message">
|
||||
<span className="message-author">You</span>
|
||||
<p>{displayedRequest}</p>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
{(viewingHistory
|
||||
? !!displayedAnswer
|
||||
: runActive || !!displayedAnswer || displayedAnswerComplete) && (
|
||||
<article
|
||||
className="quick-pane-assistant-message"
|
||||
role="region"
|
||||
aria-label="Assistant response"
|
||||
aria-live={viewingHistory ? "off" : "polite"}
|
||||
aria-busy={!viewingHistory && runActive}
|
||||
>
|
||||
{displayedAnswer && (
|
||||
<>
|
||||
<span className="message-author">Assistant</span>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{displayedAnswer}
|
||||
</ReactMarkdown>
|
||||
</>
|
||||
)}
|
||||
{displayedAnswerComplete && (
|
||||
<span className="sr-only">Response complete.</span>
|
||||
)}
|
||||
</article>
|
||||
)}
|
||||
{!viewingHistory && activity && (
|
||||
<div className="quick-pane-activity" aria-hidden="true">
|
||||
<span className="typing-dots">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span>{activity}</span>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className="quick-pane-activity-announcement sr-only"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{viewingHistory ? "" : activityPhase}
|
||||
</span>
|
||||
{escalation && (
|
||||
<aside className="quick-pane-notice escalation" role="status">
|
||||
<div>
|
||||
<strong>Continue in full session</strong>
|
||||
<p>{escalation}</p>
|
||||
</div>
|
||||
<button type="button" onClick={continueFull}>
|
||||
Continue
|
||||
</button>
|
||||
</aside>
|
||||
)}
|
||||
{error && (
|
||||
<aside className="quick-pane-notice error" role="alert">
|
||||
<p>{error}</p>
|
||||
{!!runtime && !escalation && (
|
||||
<button type="button" onClick={continueFull}>
|
||||
Continue
|
||||
</button>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
{catalogError && (
|
||||
<aside className="quick-pane-notice error" role="alert">
|
||||
<p>{catalogError}</p>
|
||||
</aside>
|
||||
)}
|
||||
{notice && (
|
||||
<p className="quick-pane-system-notice" role="status">
|
||||
{notice}
|
||||
</p>
|
||||
)}
|
||||
<ExtensionDialog extension={extension} onRespond={respond} />
|
||||
</section>
|
||||
<footer className="quick-pane-controls">
|
||||
<label>
|
||||
<span>Model</span>
|
||||
<select
|
||||
aria-label="Model"
|
||||
value={selectedModelValue}
|
||||
onChange={(event) => {
|
||||
const identity = modelIdentityFromValue(
|
||||
event.currentTarget.value,
|
||||
);
|
||||
if (identity) setSelectedModel(identity);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select model
|
||||
</option>
|
||||
{modelOptions.map((model) => {
|
||||
const identity = modelIdentity(model);
|
||||
if (!identity) return null;
|
||||
return (
|
||||
<option
|
||||
key={modelIdentityValue(identity)}
|
||||
value={modelIdentityValue(identity)}
|
||||
>
|
||||
{identity.provider}/{identity.modelId}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Thinking</span>
|
||||
<select
|
||||
aria-label="Thinking"
|
||||
value={thinkingLevel}
|
||||
onChange={(event) => setThinkingLevel(event.currentTarget.value)}
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="quick-pane-composer">
|
||||
<textarea
|
||||
className="quick-pane-request"
|
||||
aria-label="Request"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
readOnly={runActive}
|
||||
aria-busy={runActive}
|
||||
value={request}
|
||||
onChange={(event) => setRequest(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
event.preventDefault();
|
||||
void run();
|
||||
}
|
||||
}}
|
||||
placeholder="Ask a quick question or give a quick instruction"
|
||||
/>
|
||||
<button
|
||||
className="quick-pane-send"
|
||||
type="button"
|
||||
aria-label={submissionStarting ? "Starting request" : "Send request"}
|
||||
title="Send (Enter)"
|
||||
disabled={
|
||||
submissionStarting ||
|
||||
!settingsLoaded ||
|
||||
!!invalid ||
|
||||
!request.trim()
|
||||
runActive || !settingsLoaded || !!invalid || !request.trim()
|
||||
}
|
||||
onClick={run}
|
||||
>
|
||||
{submissionStarting ? "Starting…" : "Ask"}
|
||||
<span aria-hidden="true">{submissionStarting ? "•••" : "↑"}</span>
|
||||
</button>
|
||||
{canContinue && (
|
||||
<button className="quiet" onClick={continueFull}>
|
||||
Continue
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
<span className="quick-pane-composer-hint">
|
||||
Shift+Enter for newline
|
||||
</span>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user