iris-chatbot 5.0.0 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/globals.css +167 -2
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/ChatView.tsx +225 -46
- package/template/src/components/Composer.tsx +84 -54
- package/template/src/components/SettingsModal.tsx +50 -52
- package/template/src/lib/tooling/tools/music.ts +18 -5
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iris",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "iris",
|
|
9
|
-
"version": "5.0.
|
|
9
|
+
"version": "5.0.2",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.72.1",
|
|
12
12
|
"clsx": "^2.1.1",
|
package/template/package.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
body {
|
|
40
40
|
background: var(--bg);
|
|
41
41
|
color: var(--text-primary);
|
|
42
|
-
font-family: var(--app-font, var(--font-
|
|
42
|
+
font-family: var(--app-font, var(--font-poppins));
|
|
43
43
|
min-height: 100vh;
|
|
44
44
|
-webkit-font-smoothing: antialiased;
|
|
45
45
|
text-rendering: optimizeLegibility;
|
|
@@ -107,7 +107,7 @@ button:focus-visible {
|
|
|
107
107
|
pointer-events: none;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
.topbar
|
|
110
|
+
.topbar>*,
|
|
111
111
|
.topbar button,
|
|
112
112
|
.topbar a,
|
|
113
113
|
.topbar [role="listbox"],
|
|
@@ -890,8 +890,173 @@ button:focus-visible {
|
|
|
890
890
|
|
|
891
891
|
.chat-scroll {
|
|
892
892
|
padding-top: calc(var(--topbar-height) + 24px);
|
|
893
|
+
position: relative;
|
|
893
894
|
}
|
|
894
895
|
|
|
895
896
|
.chat-scroll.empty {
|
|
896
897
|
padding-top: calc(var(--topbar-height) + 24px);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/* ─── Ask Selection Button ─── */
|
|
901
|
+
|
|
902
|
+
.ask-selection-button {
|
|
903
|
+
position: absolute;
|
|
904
|
+
z-index: 50;
|
|
905
|
+
transform: translateX(-50%);
|
|
906
|
+
display: inline-flex;
|
|
907
|
+
align-items: center;
|
|
908
|
+
gap: 6px;
|
|
909
|
+
padding: 7px 14px;
|
|
910
|
+
border-radius: 10px;
|
|
911
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
912
|
+
background: rgba(50, 50, 50, 0.92);
|
|
913
|
+
backdrop-filter: blur(12px);
|
|
914
|
+
-webkit-backdrop-filter: blur(12px);
|
|
915
|
+
color: #f0f0f0;
|
|
916
|
+
font-size: 13px;
|
|
917
|
+
font-weight: 600;
|
|
918
|
+
letter-spacing: 0.01em;
|
|
919
|
+
cursor: pointer;
|
|
920
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35), 0 0 0 0.5px rgba(255, 255, 255, 0.06);
|
|
921
|
+
animation: ask-button-enter 0.18s ease-out;
|
|
922
|
+
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
|
|
923
|
+
white-space: nowrap;
|
|
924
|
+
user-select: none;
|
|
925
|
+
-webkit-user-select: none;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.ask-selection-button:hover {
|
|
929
|
+
background: rgba(60, 60, 60, 0.95);
|
|
930
|
+
border-color: rgba(255, 255, 255, 0.22);
|
|
931
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4), 0 0 0 0.5px rgba(255, 255, 255, 0.1);
|
|
932
|
+
transform: translateX(-50%) translateY(-1px);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.ask-selection-button:active {
|
|
936
|
+
transform: translateX(-50%) translateY(0);
|
|
937
|
+
background: rgba(70, 70, 70, 0.95);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
@keyframes ask-button-enter {
|
|
941
|
+
from {
|
|
942
|
+
opacity: 0;
|
|
943
|
+
transform: translateX(-50%) translateY(6px) scale(0.92);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
to {
|
|
947
|
+
opacity: 1;
|
|
948
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/* ─── Quoted Context Container ─── */
|
|
953
|
+
|
|
954
|
+
.composer-wrapper {
|
|
955
|
+
display: flex;
|
|
956
|
+
flex-direction: column;
|
|
957
|
+
gap: 0;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.quoted-context-container {
|
|
961
|
+
display: flex;
|
|
962
|
+
align-items: flex-start;
|
|
963
|
+
gap: 10px;
|
|
964
|
+
padding: 12px 14px;
|
|
965
|
+
margin-bottom: 0;
|
|
966
|
+
background: #2b2b2b;
|
|
967
|
+
border: 1px solid var(--border);
|
|
968
|
+
border-bottom: none;
|
|
969
|
+
border-radius: 18px 18px 0 0;
|
|
970
|
+
animation: quoted-context-enter 0.22s ease-out;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.quoted-context-container+.composer {
|
|
974
|
+
border-top-left-radius: 0;
|
|
975
|
+
border-top-right-radius: 0;
|
|
976
|
+
border-top-color: var(--border);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.quoted-context-content {
|
|
980
|
+
flex: 1;
|
|
981
|
+
display: flex;
|
|
982
|
+
align-items: flex-start;
|
|
983
|
+
gap: 10px;
|
|
984
|
+
min-width: 0;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.quoted-context-icon {
|
|
988
|
+
flex-shrink: 0;
|
|
989
|
+
width: 18px;
|
|
990
|
+
height: 18px;
|
|
991
|
+
color: var(--text-muted);
|
|
992
|
+
margin-top: 1px;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.quoted-context-text {
|
|
996
|
+
font-size: 13px;
|
|
997
|
+
line-height: 1.5;
|
|
998
|
+
color: var(--text-secondary);
|
|
999
|
+
overflow: hidden;
|
|
1000
|
+
display: -webkit-box;
|
|
1001
|
+
-webkit-line-clamp: 3;
|
|
1002
|
+
line-clamp: 3;
|
|
1003
|
+
-webkit-box-orient: vertical;
|
|
1004
|
+
word-break: break-word;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.quoted-context-dismiss {
|
|
1008
|
+
flex-shrink: 0;
|
|
1009
|
+
display: inline-flex;
|
|
1010
|
+
align-items: center;
|
|
1011
|
+
justify-content: center;
|
|
1012
|
+
width: 24px;
|
|
1013
|
+
height: 24px;
|
|
1014
|
+
border-radius: 999px;
|
|
1015
|
+
border: none;
|
|
1016
|
+
background: transparent;
|
|
1017
|
+
color: var(--text-muted);
|
|
1018
|
+
cursor: pointer;
|
|
1019
|
+
transition: color 0.15s ease, background 0.15s ease;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.quoted-context-dismiss:hover {
|
|
1023
|
+
color: var(--text-primary);
|
|
1024
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
@keyframes quoted-context-enter {
|
|
1028
|
+
from {
|
|
1029
|
+
opacity: 0;
|
|
1030
|
+
transform: translateY(6px);
|
|
1031
|
+
max-height: 0;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
to {
|
|
1035
|
+
opacity: 1;
|
|
1036
|
+
transform: translateY(0);
|
|
1037
|
+
max-height: 200px;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/* ─── Light theme overrides ─── */
|
|
1042
|
+
|
|
1043
|
+
[data-theme="light"] .ask-selection-button {
|
|
1044
|
+
background: rgba(255, 255, 255, 0.94);
|
|
1045
|
+
border-color: rgba(0, 0, 0, 0.12);
|
|
1046
|
+
color: #111111;
|
|
1047
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(0, 0, 0, 0.05);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
[data-theme="light"] .ask-selection-button:hover {
|
|
1051
|
+
background: #ffffff;
|
|
1052
|
+
border-color: rgba(0, 0, 0, 0.18);
|
|
1053
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15), 0 0 0 0.5px rgba(0, 0, 0, 0.08);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
[data-theme="light"] .quoted-context-container {
|
|
1057
|
+
background: transparent;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
[data-theme="light"] .quoted-context-dismiss:hover {
|
|
1061
|
+
background: rgba(0, 0, 0, 0.06);
|
|
897
1062
|
}
|
|
@@ -124,7 +124,7 @@ export default function Home() {
|
|
|
124
124
|
? "var(--font-sora)"
|
|
125
125
|
: settings.font === "space"
|
|
126
126
|
? "var(--font-space)"
|
|
127
|
-
: "var(--font-
|
|
127
|
+
: "var(--font-poppins)";
|
|
128
128
|
document.documentElement.style.setProperty("--app-font", fontVar);
|
|
129
129
|
document.body.style.fontFamily = fontVar;
|
|
130
130
|
}, [settings, connectionOverrideId, setConnectionOverrideId]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from "react";
|
|
4
4
|
import type {
|
|
5
5
|
ChatCitationSource,
|
|
6
6
|
ModelConnection,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "../lib/memory";
|
|
27
27
|
import { useUIStore } from "../lib/store";
|
|
28
28
|
import { toChatConnectionPayload } from "../lib/connections";
|
|
29
|
-
import { ArrowDown } from "lucide-react";
|
|
29
|
+
import { ArrowDown, TextQuote } from "lucide-react";
|
|
30
30
|
import MessageCard from "./MessageCard";
|
|
31
31
|
import Composer from "./Composer";
|
|
32
32
|
|
|
@@ -326,6 +326,10 @@ export default function ChatView({
|
|
|
326
326
|
const [showJumpToBottom, setShowJumpToBottom] = useState(false);
|
|
327
327
|
const [micMode, setMicMode] = useState<MicMode>("off");
|
|
328
328
|
const [micState, setMicState] = useState<MicState>("idle");
|
|
329
|
+
const [quotedContext, setQuotedContext] = useState<string | null>(null);
|
|
330
|
+
const [askButtonPos, setAskButtonPos] = useState<{ top: number; left: number } | null>(null);
|
|
331
|
+
const [pendingSelection, setPendingSelection] = useState<string | null>(null);
|
|
332
|
+
const askButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
329
333
|
const focusedMessageId = useUIStore((state) => state.focusedMessageId);
|
|
330
334
|
const setFocusedMessageId = useUIStore((state) => state.setFocusedMessageId);
|
|
331
335
|
const chatScrollRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -346,7 +350,7 @@ export default function ChatView({
|
|
|
346
350
|
const recognitionFinalTranscriptRef = useRef("");
|
|
347
351
|
const recognitionInterimTranscriptRef = useRef("");
|
|
348
352
|
const micSilenceTimerRef = useRef<number | null>(null);
|
|
349
|
-
const handleSendRef = useRef<(overrideInput?: string) => Promise<void>>(async () => {});
|
|
353
|
+
const handleSendRef = useRef<(overrideInput?: string) => Promise<void>>(async () => { });
|
|
350
354
|
|
|
351
355
|
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
|
352
356
|
const container = chatScrollRef.current;
|
|
@@ -819,12 +823,113 @@ export default function ChatView({
|
|
|
819
823
|
startMicRecognitionSession("dictation");
|
|
820
824
|
}, [micSupported, startMicRecognitionSession, stopMicMode]);
|
|
821
825
|
|
|
826
|
+
const handleTextSelection = useCallback(() => {
|
|
827
|
+
const selection = window.getSelection();
|
|
828
|
+
if (!selection || selection.isCollapsed || !selection.toString().trim()) {
|
|
829
|
+
setAskButtonPos(null);
|
|
830
|
+
setPendingSelection(null);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const selectedText = selection.toString().trim();
|
|
835
|
+
if (!selectedText) {
|
|
836
|
+
setAskButtonPos(null);
|
|
837
|
+
setPendingSelection(null);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Check if the selection is inside an assistant message
|
|
842
|
+
let node: Node | null = selection.anchorNode;
|
|
843
|
+
let isInAssistantMessage = false;
|
|
844
|
+
while (node) {
|
|
845
|
+
if (node instanceof HTMLElement) {
|
|
846
|
+
if (node.classList.contains("assistant-card") || node.closest?.(".assistant-card")) {
|
|
847
|
+
isInAssistantMessage = true;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
if (node.classList.contains("message-row") && node.classList.contains("assistant")) {
|
|
851
|
+
isInAssistantMessage = true;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
node = node.parentNode;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (!isInAssistantMessage) {
|
|
859
|
+
setAskButtonPos(null);
|
|
860
|
+
setPendingSelection(null);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const range = selection.getRangeAt(0);
|
|
865
|
+
const rect = range.getBoundingClientRect();
|
|
866
|
+
const scrollContainer = chatScrollRef.current;
|
|
867
|
+
|
|
868
|
+
if (scrollContainer) {
|
|
869
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
870
|
+
setAskButtonPos({
|
|
871
|
+
top: rect.top - containerRect.top + scrollContainer.scrollTop - 44,
|
|
872
|
+
left: rect.left - containerRect.left + rect.width / 2,
|
|
873
|
+
});
|
|
874
|
+
} else {
|
|
875
|
+
setAskButtonPos({
|
|
876
|
+
top: rect.top + window.scrollY - 44,
|
|
877
|
+
left: rect.left + rect.width / 2,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
setPendingSelection(selectedText);
|
|
881
|
+
}, []);
|
|
882
|
+
|
|
883
|
+
const handleAskClick = useCallback((event: ReactMouseEvent) => {
|
|
884
|
+
event.preventDefault();
|
|
885
|
+
event.stopPropagation();
|
|
886
|
+
if (pendingSelection) {
|
|
887
|
+
setQuotedContext(pendingSelection);
|
|
888
|
+
setAskButtonPos(null);
|
|
889
|
+
setPendingSelection(null);
|
|
890
|
+
window.getSelection()?.removeAllRanges();
|
|
891
|
+
}
|
|
892
|
+
}, [pendingSelection]);
|
|
893
|
+
|
|
894
|
+
const handleClearQuotedContext = useCallback(() => {
|
|
895
|
+
setQuotedContext(null);
|
|
896
|
+
}, []);
|
|
897
|
+
|
|
898
|
+
useEffect(() => {
|
|
899
|
+
const onMouseUp = () => {
|
|
900
|
+
// Small delay so the selection is finalized
|
|
901
|
+
setTimeout(handleTextSelection, 10);
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const onSelectionChange = () => {
|
|
905
|
+
const selection = window.getSelection();
|
|
906
|
+
if (!selection || selection.isCollapsed) {
|
|
907
|
+
// Don't dismiss if the ask button itself was clicked
|
|
908
|
+
setTimeout(() => {
|
|
909
|
+
const currentSelection = window.getSelection();
|
|
910
|
+
if (!currentSelection || currentSelection.isCollapsed) {
|
|
911
|
+
setAskButtonPos(null);
|
|
912
|
+
setPendingSelection(null);
|
|
913
|
+
}
|
|
914
|
+
}, 150);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
919
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
920
|
+
return () => {
|
|
921
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
922
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
923
|
+
};
|
|
924
|
+
}, [handleTextSelection]);
|
|
925
|
+
|
|
822
926
|
const handleSend = useCallback(async (overrideInput?: string) => {
|
|
823
927
|
const resolvedThread =
|
|
824
928
|
thread || (activeThreadId ? await db.threads.get(activeThreadId) : null);
|
|
825
929
|
if (!resolvedThread) return;
|
|
826
930
|
const trimmed = (overrideInput ?? input).trim();
|
|
827
|
-
|
|
931
|
+
const hasQuoted = Boolean(quotedContext);
|
|
932
|
+
if (!trimmed && !hasQuoted) return;
|
|
828
933
|
const sendTriggeredByMic =
|
|
829
934
|
typeof overrideInput === "string" || micModeRef.current !== "off";
|
|
830
935
|
const stopMicAfterValidationFailure = () => {
|
|
@@ -886,6 +991,8 @@ export default function ChatView({
|
|
|
886
991
|
inflightThreadIdsRef.current.add(resolvedThread.id);
|
|
887
992
|
setError(null);
|
|
888
993
|
setInput("");
|
|
994
|
+
const capturedQuotedContext = quotedContext;
|
|
995
|
+
setQuotedContext(null);
|
|
889
996
|
setFocusedMessageId(null);
|
|
890
997
|
pendingSendScrollThreadIdRef.current = resolvedThread.id;
|
|
891
998
|
isUserNearBottomRef.current = true;
|
|
@@ -894,9 +1001,15 @@ export default function ChatView({
|
|
|
894
1001
|
smoothFollowUntilRef.current = performance.now() + 1200;
|
|
895
1002
|
scrollToBottom("smooth");
|
|
896
1003
|
|
|
1004
|
+
const contentToSend = capturedQuotedContext
|
|
1005
|
+
? trimmed
|
|
1006
|
+
? `Regarding this text: "${capturedQuotedContext}"\n\n${trimmed}`
|
|
1007
|
+
: `Explain this: "${capturedQuotedContext}"`
|
|
1008
|
+
: trimmed;
|
|
1009
|
+
|
|
897
1010
|
const { userMessage, assistantMessage } = await appendUserAndAssistant({
|
|
898
1011
|
thread: resolvedThread,
|
|
899
|
-
content:
|
|
1012
|
+
content: contentToSend,
|
|
900
1013
|
provider: connection.kind === "builtin" ? connection.provider ?? connection.id : connection.id,
|
|
901
1014
|
model,
|
|
902
1015
|
});
|
|
@@ -906,8 +1019,8 @@ export default function ChatView({
|
|
|
906
1019
|
const lastAssistantMessageContent =
|
|
907
1020
|
lastAssistantMessage?.role === "assistant"
|
|
908
1021
|
? (typeof lastAssistantMessage.content === "string"
|
|
909
|
-
|
|
910
|
-
|
|
1022
|
+
? lastAssistantMessage.content
|
|
1023
|
+
: "")
|
|
911
1024
|
: undefined;
|
|
912
1025
|
const memoryEnabled = settings.memory?.enabled !== false;
|
|
913
1026
|
const memoryAutoCaptureEnabled =
|
|
@@ -971,10 +1084,10 @@ export default function ChatView({
|
|
|
971
1084
|
|
|
972
1085
|
const siblingThreadsContext = shouldIncludeSiblingThreadContext(trimmed)
|
|
973
1086
|
? buildSiblingThreadsContext({
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1087
|
+
allThreads: threads,
|
|
1088
|
+
activeThread: resolvedThread,
|
|
1089
|
+
messageMap: map,
|
|
1090
|
+
})
|
|
978
1091
|
: null;
|
|
979
1092
|
if (siblingThreadsContext) {
|
|
980
1093
|
payloadMessages.push({
|
|
@@ -987,8 +1100,54 @@ export default function ChatView({
|
|
|
987
1100
|
|
|
988
1101
|
payloadMessages.push(
|
|
989
1102
|
...history
|
|
990
|
-
|
|
991
|
-
|
|
1103
|
+
.filter((message) => message.role !== "system")
|
|
1104
|
+
.map((message) => {
|
|
1105
|
+
let content = message.content;
|
|
1106
|
+
|
|
1107
|
+
// Enrich assistant messages with tool call summaries so the model
|
|
1108
|
+
// retains awareness of tools it successfully used earlier in the
|
|
1109
|
+
// conversation. Without this, the model "forgets" its own tool
|
|
1110
|
+
// capabilities and may deny being able to do things it already did.
|
|
1111
|
+
if (message.role === "assistant") {
|
|
1112
|
+
const events = toolEventsByMessage.get(message.id);
|
|
1113
|
+
if (events && events.length > 0) {
|
|
1114
|
+
const resultEvents = events.filter((e) => e.stage === "result");
|
|
1115
|
+
if (resultEvents.length > 0) {
|
|
1116
|
+
const summaries = resultEvents.map((e) => {
|
|
1117
|
+
const name = e.toolName || "Unknown Tool";
|
|
1118
|
+
// Try to extract a meaningful result summary from payloadJson
|
|
1119
|
+
let resultSummary = "";
|
|
1120
|
+
if (e.payloadJson) {
|
|
1121
|
+
try {
|
|
1122
|
+
const payload = JSON.parse(e.payloadJson);
|
|
1123
|
+
if (typeof payload === "object" && payload !== null) {
|
|
1124
|
+
// Look for common result fields
|
|
1125
|
+
resultSummary =
|
|
1126
|
+
payload.message || payload.summary || payload.result ||
|
|
1127
|
+
payload.status || payload.output || "";
|
|
1128
|
+
if (typeof resultSummary !== "string") {
|
|
1129
|
+
resultSummary = JSON.stringify(payload).slice(0, 120);
|
|
1130
|
+
}
|
|
1131
|
+
} else if (typeof payload === "string") {
|
|
1132
|
+
resultSummary = payload;
|
|
1133
|
+
}
|
|
1134
|
+
} catch {
|
|
1135
|
+
// payloadJson may not be valid JSON; ignore
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (!resultSummary && e.message && e.message !== "success") {
|
|
1139
|
+
resultSummary = e.message;
|
|
1140
|
+
}
|
|
1141
|
+
return resultSummary ? `${name} → ${resultSummary}` : name;
|
|
1142
|
+
});
|
|
1143
|
+
const toolPrefix = `[Tools used: ${summaries.join("; ")}]`;
|
|
1144
|
+
content = content ? `${toolPrefix}\n\n${content}` : toolPrefix;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return { role: message.role, content };
|
|
1150
|
+
}),
|
|
992
1151
|
);
|
|
993
1152
|
|
|
994
1153
|
const controller = new AbortController();
|
|
@@ -1363,7 +1522,9 @@ export default function ChatView({
|
|
|
1363
1522
|
stopMicRecognitionSession,
|
|
1364
1523
|
startMicRecognitionSession,
|
|
1365
1524
|
stopMicMode,
|
|
1525
|
+
quotedContext,
|
|
1366
1526
|
messageMap,
|
|
1527
|
+
toolEventsByMessage,
|
|
1367
1528
|
]);
|
|
1368
1529
|
|
|
1369
1530
|
useEffect(() => {
|
|
@@ -1472,9 +1633,8 @@ export default function ChatView({
|
|
|
1472
1633
|
<div
|
|
1473
1634
|
ref={chatScrollRef}
|
|
1474
1635
|
onScroll={updateBottomVisibility}
|
|
1475
|
-
className={`chat-scroll flex-1 overflow-y-auto px-6 py-8${
|
|
1476
|
-
|
|
1477
|
-
}`}
|
|
1636
|
+
className={`chat-scroll flex-1 overflow-y-auto px-6 py-8${threadMessages.length === 0 ? " empty" : ""
|
|
1637
|
+
}`}
|
|
1478
1638
|
>
|
|
1479
1639
|
{threadMessages.length === 0 ? (
|
|
1480
1640
|
<div className="empty-hero">
|
|
@@ -1493,6 +1653,8 @@ export default function ChatView({
|
|
|
1493
1653
|
onMicToggle={handleMicToggle}
|
|
1494
1654
|
micState={micState}
|
|
1495
1655
|
micDisabled={Boolean(activeStreamingMessageId)}
|
|
1656
|
+
quotedContext={quotedContext}
|
|
1657
|
+
onClearQuotedContext={handleClearQuotedContext}
|
|
1496
1658
|
/>
|
|
1497
1659
|
</div>
|
|
1498
1660
|
</div>
|
|
@@ -1501,21 +1663,36 @@ export default function ChatView({
|
|
|
1501
1663
|
{renderedThreadRows}
|
|
1502
1664
|
</div>
|
|
1503
1665
|
)}
|
|
1666
|
+
{askButtonPos ? (
|
|
1667
|
+
<button
|
|
1668
|
+
ref={askButtonRef}
|
|
1669
|
+
className="ask-selection-button"
|
|
1670
|
+
style={{
|
|
1671
|
+
top: askButtonPos.top,
|
|
1672
|
+
left: askButtonPos.left,
|
|
1673
|
+
}}
|
|
1674
|
+
onMouseDown={handleAskClick}
|
|
1675
|
+
aria-label="Ask about selection"
|
|
1676
|
+
>
|
|
1677
|
+
<TextQuote className="h-3.5 w-3.5" />
|
|
1678
|
+
Ask
|
|
1679
|
+
</button>
|
|
1680
|
+
) : null}
|
|
1504
1681
|
<div ref={bottomRef} className="h-2" />
|
|
1505
1682
|
</div>
|
|
1506
1683
|
{threadMessages.length > 0 ? (
|
|
1507
|
-
|
|
1508
|
-
|
|
1684
|
+
<>
|
|
1685
|
+
{showJumpToBottom ? (
|
|
1509
1686
|
<div className="pointer-events-none absolute bottom-28 left-1/2 z-30 -translate-x-1/2 opacity-80 transition-opacity duration-200">
|
|
1510
1687
|
<button
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1688
|
+
className="pointer-events-auto inline-flex h-9 w-9 items-center justify-center rounded-full border border-[var(--border-strong)] bg-[var(--panel)] text-[var(--text-primary)] shadow-[var(--shadow)] transition hover:translate-y-[1px] hover:border-[var(--accent)] hover:opacity-100"
|
|
1689
|
+
onClick={() => {
|
|
1690
|
+
shouldStickToBottomRef.current = true;
|
|
1691
|
+
setShowJumpToBottom(false);
|
|
1692
|
+
smoothFollowUntilRef.current = performance.now() + 900;
|
|
1693
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
|
1694
|
+
}}
|
|
1695
|
+
aria-label="Jump to latest message"
|
|
1519
1696
|
title="Jump to latest"
|
|
1520
1697
|
>
|
|
1521
1698
|
<ArrowDown className="h-4 w-4" />
|
|
@@ -1523,27 +1700,29 @@ export default function ChatView({
|
|
|
1523
1700
|
</div>
|
|
1524
1701
|
) : null}
|
|
1525
1702
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1703
|
+
<div className="composer-bar px-6 py-4">
|
|
1704
|
+
<div className="mx-auto w-full max-w-3xl">
|
|
1705
|
+
{error ? (
|
|
1706
|
+
<div className="mb-3 rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-xs text-[var(--danger)]">
|
|
1707
|
+
{error}
|
|
1708
|
+
</div>
|
|
1709
|
+
) : null}
|
|
1710
|
+
<Composer
|
|
1711
|
+
value={input}
|
|
1712
|
+
onChange={setInput}
|
|
1713
|
+
onSend={() => {
|
|
1714
|
+
void handleSend();
|
|
1715
|
+
}}
|
|
1716
|
+
onStop={handleStop}
|
|
1717
|
+
isStreaming={Boolean(activeStreamingMessageId)}
|
|
1718
|
+
onMicToggle={handleMicToggle}
|
|
1719
|
+
micState={micState}
|
|
1720
|
+
micDisabled={Boolean(activeStreamingMessageId)}
|
|
1721
|
+
quotedContext={quotedContext}
|
|
1722
|
+
onClearQuotedContext={handleClearQuotedContext}
|
|
1723
|
+
/>
|
|
1724
|
+
</div>
|
|
1545
1725
|
</div>
|
|
1546
|
-
</div>
|
|
1547
1726
|
</>
|
|
1548
1727
|
) : null}
|
|
1549
1728
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
|
4
|
-
import { ArrowUp, Loader2, Mic, Square } from "lucide-react";
|
|
4
|
+
import { ArrowUp, CornerDownRight, Loader2, Mic, Square, X } from "lucide-react";
|
|
5
5
|
|
|
6
6
|
function normalizePastedText(input: string): string {
|
|
7
7
|
return input
|
|
@@ -21,6 +21,8 @@ export default function Composer({
|
|
|
21
21
|
onMicToggle,
|
|
22
22
|
micState = "idle",
|
|
23
23
|
micDisabled = false,
|
|
24
|
+
quotedContext = null,
|
|
25
|
+
onClearQuotedContext,
|
|
24
26
|
}: {
|
|
25
27
|
value: string;
|
|
26
28
|
onChange: (value: string) => void;
|
|
@@ -30,6 +32,8 @@ export default function Composer({
|
|
|
30
32
|
onMicToggle?: () => void;
|
|
31
33
|
micState?: "idle" | "listening" | "processing";
|
|
32
34
|
micDisabled?: boolean;
|
|
35
|
+
quotedContext?: string | null;
|
|
36
|
+
onClearQuotedContext?: () => void;
|
|
33
37
|
}) {
|
|
34
38
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
35
39
|
const minRowsRef = useRef(1);
|
|
@@ -109,61 +113,87 @@ export default function Composer({
|
|
|
109
113
|
};
|
|
110
114
|
}, [resizeTextarea, value]);
|
|
111
115
|
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (quotedContext && textareaRef.current) {
|
|
118
|
+
textareaRef.current.focus();
|
|
119
|
+
}
|
|
120
|
+
}, [quotedContext]);
|
|
121
|
+
|
|
112
122
|
return (
|
|
113
|
-
<div className="composer
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
131
|
-
event.preventDefault();
|
|
132
|
-
if (!isStreaming) onSend();
|
|
133
|
-
}
|
|
134
|
-
}}
|
|
135
|
-
/>
|
|
136
|
-
{onMicToggle ? (
|
|
137
|
-
<button
|
|
138
|
-
onClick={onMicToggle}
|
|
139
|
-
className={`send-button mic-button shrink-0 ${micState !== "idle" ? "active" : ""}`}
|
|
140
|
-
disabled={micDisabled || micState === "processing"}
|
|
141
|
-
title={micButtonTitle}
|
|
142
|
-
aria-label={micButtonTitle}
|
|
143
|
-
>
|
|
144
|
-
{micState === "processing" ? (
|
|
145
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
146
|
-
) : (
|
|
147
|
-
<Mic className="h-4 w-4" />
|
|
148
|
-
)}
|
|
149
|
-
</button>
|
|
123
|
+
<div className="composer-wrapper">
|
|
124
|
+
{quotedContext ? (
|
|
125
|
+
<div className="quoted-context-container">
|
|
126
|
+
<div className="quoted-context-content">
|
|
127
|
+
<CornerDownRight className="quoted-context-icon" />
|
|
128
|
+
<span className="quoted-context-text">
|
|
129
|
+
“{quotedContext.length > 280 ? `${quotedContext.slice(0, 280)}…` : quotedContext}”
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
<button
|
|
133
|
+
className="quoted-context-dismiss"
|
|
134
|
+
onClick={onClearQuotedContext}
|
|
135
|
+
aria-label="Remove quoted context"
|
|
136
|
+
>
|
|
137
|
+
<X className="h-3.5 w-3.5" />
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
150
140
|
) : null}
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
141
|
+
<div className="composer flex items-end gap-3">
|
|
142
|
+
<textarea
|
|
143
|
+
ref={textareaRef}
|
|
144
|
+
rows={1}
|
|
145
|
+
className="composer-textarea flex-1 text-base text-[var(--text-primary)]"
|
|
146
|
+
value={value}
|
|
147
|
+
onChange={(event) => onChange(event.target.value)}
|
|
148
|
+
onPaste={(event) => {
|
|
149
|
+
const clipboardText = event.clipboardData.getData("text/plain");
|
|
150
|
+
if (!clipboardText) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
insertAtCursor(normalizePastedText(clipboardText));
|
|
155
|
+
}}
|
|
156
|
+
placeholder={quotedContext ? "Ask about this selection" : "Ask anything"}
|
|
157
|
+
onKeyDown={(event) => {
|
|
158
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
if (!isStreaming) onSend();
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
{onMicToggle ? (
|
|
165
|
+
<button
|
|
166
|
+
onClick={onMicToggle}
|
|
167
|
+
className={`send-button mic-button shrink-0 ${micState !== "idle" ? "active" : ""}`}
|
|
168
|
+
disabled={micDisabled || micState === "processing"}
|
|
169
|
+
title={micButtonTitle}
|
|
170
|
+
aria-label={micButtonTitle}
|
|
171
|
+
>
|
|
172
|
+
{micState === "processing" ? (
|
|
173
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
174
|
+
) : (
|
|
175
|
+
<Mic className="h-4 w-4" />
|
|
176
|
+
)}
|
|
177
|
+
</button>
|
|
178
|
+
) : null}
|
|
179
|
+
{isStreaming ? (
|
|
180
|
+
<button
|
|
181
|
+
onClick={onStop}
|
|
182
|
+
className="flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-xs text-[var(--text-secondary)]"
|
|
183
|
+
>
|
|
184
|
+
<Square className="h-3 w-3" />
|
|
185
|
+
Stop
|
|
186
|
+
</button>
|
|
187
|
+
) : (
|
|
188
|
+
<button
|
|
189
|
+
onClick={onSend}
|
|
190
|
+
className={`send-button shrink-0 ${hasText || quotedContext ? "active" : ""}`}
|
|
191
|
+
>
|
|
192
|
+
<ArrowUp className="h-4 w-4" />
|
|
193
|
+
</button>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
167
196
|
</div>
|
|
168
197
|
);
|
|
169
198
|
}
|
|
199
|
+
|
|
@@ -115,7 +115,7 @@ export default function SettingsModal({
|
|
|
115
115
|
const [accentColor, setAccentColor] = useState(settings?.accentColor || DEFAULT_ACCENT_DARK);
|
|
116
116
|
const [theme, setTheme] = useState<"dark" | "light">(settings?.theme || "dark");
|
|
117
117
|
const [font, setFont] = useState<"ibm" | "manrope" | "sora" | "space" | "poppins">(
|
|
118
|
-
settings?.font || "
|
|
118
|
+
settings?.font || "poppins",
|
|
119
119
|
);
|
|
120
120
|
|
|
121
121
|
const localTools = settings?.localTools ?? DEFAULT_LOCAL_TOOLS_SETTINGS;
|
|
@@ -195,8 +195,8 @@ export default function SettingsModal({
|
|
|
195
195
|
|
|
196
196
|
const selectedDefaultModel = selectedDefaultConnection
|
|
197
197
|
? defaultModelByConnection[selectedDefaultConnection.id] ||
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
getConnectionModelPresets(selectedDefaultConnection)[0] ||
|
|
199
|
+
""
|
|
200
200
|
: "";
|
|
201
201
|
const selectedPresets = selectedDefaultConnection ? getConnectionModelPresets(selectedDefaultConnection) : [];
|
|
202
202
|
const selectedFetchedModels = selectedDefaultConnection ? fetchedModels[selectedDefaultConnection.id] ?? [] : [];
|
|
@@ -586,11 +586,10 @@ export default function SettingsModal({
|
|
|
586
586
|
<button
|
|
587
587
|
key={tab.id}
|
|
588
588
|
onClick={() => setActiveTab(tab.id)}
|
|
589
|
-
className={`rounded-full px-3 py-1.5 text-xs settings-tab ${
|
|
590
|
-
activeTab === tab.id
|
|
589
|
+
className={`rounded-full px-3 py-1.5 text-xs settings-tab ${activeTab === tab.id
|
|
591
590
|
? "settings-tab-active"
|
|
592
591
|
: "border border-[var(--border)] bg-[var(--panel-2)] text-[var(--text-secondary)]"
|
|
593
|
-
|
|
592
|
+
}`}
|
|
594
593
|
>
|
|
595
594
|
{tab.label}
|
|
596
595
|
</button>
|
|
@@ -673,7 +672,7 @@ export default function SettingsModal({
|
|
|
673
672
|
) : null}
|
|
674
673
|
</div>
|
|
675
674
|
{selectedDefaultConnection?.kind === "builtin" &&
|
|
676
|
-
|
|
675
|
+
selectedDefaultConnection?.provider === "openai" ? (
|
|
677
676
|
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
678
677
|
<input
|
|
679
678
|
type="checkbox"
|
|
@@ -822,37 +821,37 @@ export default function SettingsModal({
|
|
|
822
821
|
/>
|
|
823
822
|
Enabled
|
|
824
823
|
</label>
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
824
|
+
<span>
|
|
825
|
+
Tools: {(connection.supportsTools ?? true) ? "supported" : "disabled"}
|
|
826
|
+
</span>
|
|
828
827
|
{connection.kind === "builtin" ? (
|
|
829
828
|
<span>Keys configured in API Keys tab</span>
|
|
830
829
|
) : null}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
830
|
+
{connectionStatus[connection.id] ? <span>{connectionStatus[connection.id]}</span> : null}
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
))}
|
|
834
|
+
</div>
|
|
836
835
|
|
|
837
836
|
<div className="rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4">
|
|
838
837
|
<div className="mb-3 text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
839
838
|
{editingConnectionId ? "Edit Connection" : "Add Connection"}
|
|
840
839
|
</div>
|
|
841
|
-
|
|
840
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
842
841
|
<input
|
|
843
842
|
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
844
843
|
placeholder="Connection name"
|
|
845
844
|
value={connectionFormName}
|
|
846
845
|
onChange={(event) => setConnectionFormName(event.target.value)}
|
|
847
846
|
/>
|
|
848
|
-
|
|
847
|
+
<select
|
|
849
848
|
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
850
849
|
value={connectionFormKind}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
850
|
+
onChange={(event) => setConnectionFormKind(event.target.value as ConnectionKind)}
|
|
851
|
+
>
|
|
852
|
+
<option value="openai_compatible">OpenAI-compatible</option>
|
|
853
|
+
<option value="ollama">Ollama</option>
|
|
854
|
+
</select>
|
|
856
855
|
<input
|
|
857
856
|
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm md:col-span-2"
|
|
858
857
|
placeholder="Base URL (e.g. http://localhost:11434)"
|
|
@@ -871,11 +870,11 @@ export default function SettingsModal({
|
|
|
871
870
|
value={connectionFormHeadersText}
|
|
872
871
|
onChange={(event) => setConnectionFormHeadersText(event.target.value)}
|
|
873
872
|
/>
|
|
874
|
-
|
|
873
|
+
</div>
|
|
875
874
|
{connectionStatus.form ? (
|
|
876
875
|
<div className="mt-2 text-xs text-[var(--danger)]">{connectionStatus.form}</div>
|
|
877
876
|
) : null}
|
|
878
|
-
|
|
877
|
+
<div className="mt-3 flex flex-wrap items-center gap-3">
|
|
879
878
|
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
880
879
|
<input
|
|
881
880
|
type="checkbox"
|
|
@@ -975,28 +974,28 @@ export default function SettingsModal({
|
|
|
975
974
|
placeholder="~/iris-project ~/Desktop"
|
|
976
975
|
/>
|
|
977
976
|
</div>
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
977
|
+
<div className="grid gap-2 md:grid-cols-2">
|
|
978
|
+
{[
|
|
979
|
+
{ label: "Enable Apple Notes tools", checked: enableNotes, setter: setEnableNotes },
|
|
980
|
+
{ label: "Enable app open/focus tools", checked: enableApps, setter: setEnableApps },
|
|
981
|
+
{ label: "Enable Numbers tools", checked: enableNumbers, setter: setEnableNumbers },
|
|
982
|
+
{ label: "Enable web search/open tools", checked: enableWeb, setter: setEnableWeb },
|
|
983
|
+
{ label: "Enable music controls", checked: enableMusic, setter: setEnableMusic },
|
|
984
|
+
{ label: "Enable calendar/reminders tools", checked: enableCalendar, setter: setEnableCalendar },
|
|
985
|
+
{ label: "Enable mail/messages tools", checked: enableMail, setter: setEnableMail },
|
|
986
|
+
{ label: "Enable workflow tool", checked: enableWorkflow, setter: setEnableWorkflow },
|
|
987
|
+
{ label: "Enable system controls", checked: enableSystem, setter: setEnableSystem },
|
|
988
|
+
].map((item) => (
|
|
989
|
+
<label key={item.label} className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
990
|
+
<input
|
|
991
|
+
type="checkbox"
|
|
992
|
+
checked={item.checked}
|
|
993
|
+
onChange={(event) => item.setter(event.target.checked)}
|
|
994
|
+
/>
|
|
995
|
+
{item.label}
|
|
996
|
+
</label>
|
|
997
|
+
))}
|
|
998
|
+
</div>
|
|
1000
999
|
</div>
|
|
1001
1000
|
) : null}
|
|
1002
1001
|
|
|
@@ -1226,14 +1225,14 @@ export default function SettingsModal({
|
|
|
1226
1225
|
? "var(--font-sora)"
|
|
1227
1226
|
: value === "space"
|
|
1228
1227
|
? "var(--font-space)"
|
|
1229
|
-
: "var(--font-
|
|
1228
|
+
: "var(--font-poppins)";
|
|
1230
1229
|
document.documentElement.style.setProperty("--app-font", fontVar);
|
|
1231
1230
|
document.body.style.fontFamily = fontVar;
|
|
1232
1231
|
}}
|
|
1233
1232
|
>
|
|
1234
|
-
<option value="ibm">IBM Plex Sans
|
|
1233
|
+
<option value="ibm">IBM Plex Sans</option>
|
|
1235
1234
|
<option value="manrope">Manrope</option>
|
|
1236
|
-
<option value="poppins">Poppins</option>
|
|
1235
|
+
<option value="poppins">Poppins (Default)</option>
|
|
1237
1236
|
<option value="sora">Sora</option>
|
|
1238
1237
|
<option value="space">Space Grotesk</option>
|
|
1239
1238
|
</select>
|
|
@@ -1255,11 +1254,10 @@ export default function SettingsModal({
|
|
|
1255
1254
|
return (
|
|
1256
1255
|
<button
|
|
1257
1256
|
key={preset.id}
|
|
1258
|
-
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs ${
|
|
1259
|
-
isSelected
|
|
1257
|
+
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs ${isSelected
|
|
1260
1258
|
? "border-[var(--accent-ring)] text-[var(--text-primary)]"
|
|
1261
1259
|
: "border-[var(--border)] text-[var(--text-secondary)]"
|
|
1262
|
-
|
|
1260
|
+
}`}
|
|
1263
1261
|
onClick={() =>
|
|
1264
1262
|
setAccentColor(isDefault ? DEFAULT_ACCENT_DARK : preset.color)
|
|
1265
1263
|
}
|
|
@@ -900,11 +900,24 @@ async function runMusicSetVolume(input: unknown, context: ToolExecutionContext)
|
|
|
900
900
|
return { dryRun: true, action: "music_set_volume", target, level };
|
|
901
901
|
}
|
|
902
902
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
903
|
+
// Always set the system output volume so the user hears an audible change.
|
|
904
|
+
// The Music app's internal sound volume is often already at 100%, so
|
|
905
|
+
// only setting that has no effect on actual output loudness.
|
|
906
|
+
await runAppleScript(`set volume output volume ${level}`, [], context.signal);
|
|
907
|
+
|
|
908
|
+
// If targeted at Music, also set the Music app's internal volume.
|
|
909
|
+
if (target === "music") {
|
|
910
|
+
try {
|
|
911
|
+
await runAppleScript(
|
|
912
|
+
`tell application "Music" to set sound volume to ${level}`,
|
|
913
|
+
[],
|
|
914
|
+
context.signal,
|
|
915
|
+
);
|
|
916
|
+
} catch {
|
|
917
|
+
// Music app may not be running; system volume is already set.
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
908
921
|
return { target, level };
|
|
909
922
|
}
|
|
910
923
|
|