Compare commits
2 Commits
1166a65505
...
5823fbfa72
| Author | SHA1 | Date |
|---|---|---|
|
|
5823fbfa72 | |
|
|
22f616ab2a |
|
|
@ -883,7 +883,32 @@ export const PromptInputTextarea = ({
|
||||||
if (!submitOnEnter) {
|
if (!submitOnEnter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||||
|
// Keep newline behavior explicit for modified-Enter combos.
|
||||||
|
// This avoids accidental submit shortcuts swallowing Ctrl/Cmd+Enter.
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = e.currentTarget;
|
||||||
|
const start = target.selectionStart ?? target.value.length;
|
||||||
|
const end = target.selectionEnd ?? target.value.length;
|
||||||
|
const nextValue =
|
||||||
|
target.value.slice(0, start) + "\n" + target.value.slice(end);
|
||||||
|
|
||||||
|
if (controller) {
|
||||||
|
controller.textInput.setInput(nextValue);
|
||||||
|
} else {
|
||||||
|
target.value = nextValue;
|
||||||
|
const inputEvent = new Event("input", { bubbles: true });
|
||||||
|
target.dispatchEvent(inputEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place caret right after the inserted newline.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
target.selectionStart = start + 1;
|
||||||
|
target.selectionEnd = start + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
|
||||||
) : (
|
) : (
|
||||||
<div className="text-primary ml-2 cursor-default font-serif">
|
<div className="text-primary ml-2 cursor-default font-serif">
|
||||||
{/* TODO: 测试标识 */}
|
{/* TODO: 测试标识 */}
|
||||||
XClaw <span className="text-sm text-[#000000c5]">v3.2.3</span>
|
XClaw <span className="text-sm text-[#000000c5]">v3.2.4</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ export type LegacyThreadStreamOptions = {
|
||||||
const STREAM_ERROR_FALLBACK_MESSAGE = "Request failed.";
|
const STREAM_ERROR_FALLBACK_MESSAGE = "Request failed.";
|
||||||
const STREAM_ERROR_TOAST_MESSAGE = "出现了某些错误。";
|
const STREAM_ERROR_TOAST_MESSAGE = "出现了某些错误。";
|
||||||
const STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS = 2000;
|
const STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS = 2000;
|
||||||
|
const STREAM_CANCEL_PATTERNS = [
|
||||||
|
/\bcancellederror\b/i,
|
||||||
|
/\bcancelled\b/i,
|
||||||
|
/\bcanceled\b/i,
|
||||||
|
/\babort(?:ed|error)?\b/i,
|
||||||
|
];
|
||||||
|
|
||||||
function readMessageCandidate(value: unknown): string | null {
|
function readMessageCandidate(value: unknown): string | null {
|
||||||
if (typeof value === "string" && value.trim()) {
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
|
@ -116,6 +122,21 @@ function getStreamErrorMessage(error: unknown): string {
|
||||||
return STREAM_ERROR_FALLBACK_MESSAGE;
|
return STREAM_ERROR_FALLBACK_MESSAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStreamCancellation(error: unknown, message: string): boolean {
|
||||||
|
const direct =
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"name" in error &&
|
||||||
|
typeof Reflect.get(error, "name") === "string"
|
||||||
|
? String(Reflect.get(error, "name"))
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const candidates = [message, direct];
|
||||||
|
return candidates.some((value) =>
|
||||||
|
STREAM_CANCEL_PATTERNS.some((pattern) => pattern.test(value)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeThreadId(
|
function normalizeThreadId(
|
||||||
value: string | null | undefined,
|
value: string | null | undefined,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
|
@ -234,6 +255,11 @@ export function useThreadStream({
|
||||||
|
|
||||||
const showStreamErrorToast = useCallback((error: unknown) => {
|
const showStreamErrorToast = useCallback((error: unknown) => {
|
||||||
const message = getStreamErrorMessage(error);
|
const message = getStreamErrorMessage(error);
|
||||||
|
if (isStreamCancellation(error, message)) {
|
||||||
|
// Cancellation is expected when user presses "Stop" or stream disconnects.
|
||||||
|
console.info("[useThreadStream] stream cancelled:", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastToast = lastErrorToastRef.current;
|
const lastToast = lastErrorToastRef.current;
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue