@circuitwall/jarela 2.3.0 → 2.5.0
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -5
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +14 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/access/[identity]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/access/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/display-filters/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/thread/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/[hostname]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/artifacts/lifecycle/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/[remote_jid]/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +27 -33
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/[route_id]/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js +9 -2
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js +19 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/dashboard/metrics/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/settings/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/env-sync/allowlist/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/env-sync/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/events/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/browser/activate/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/foreground/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/history/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/poll/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/progress/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/result/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/retry/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/tabs/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/harnesses/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/default/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/health/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/integrations/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/lifecycle/adoption/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/maps/embed/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/memory/[namespace]/[key]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/policy/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/compact-threads/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +27 -12
- package/.next/standalone/.next/server/app/api/v1/profile/location/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/profile/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +585 -58
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/github-copilot/auth/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/proxy-config/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/init/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/idle-timeout/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/state/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/unlock/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/verify-pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/[agent_id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/activity/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/context-pin/route.js +12 -6
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +125 -26
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -2
- package/.next/standalone/.next/server/app/api/v1/threads/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/voice/transcribe/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/voice/tts/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
- package/.next/standalone/.next/server/app/page.js +27323 -27034
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page.js +8 -0
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -5
- package/.next/standalone/.next/server/chunks/1388.js +106 -36
- package/.next/standalone/.next/server/chunks/2068.js +47 -10
- package/.next/standalone/.next/server/chunks/210.js +6 -6
- package/.next/standalone/.next/server/chunks/2447.js +10 -5
- package/.next/standalone/.next/server/chunks/2653.js +80 -1
- package/.next/standalone/.next/server/chunks/3042.js +10 -8
- package/.next/standalone/.next/server/chunks/{4540.js → 309.js} +104 -38
- package/.next/standalone/.next/server/chunks/{2350.js → 3272.js} +104 -37
- package/.next/standalone/.next/server/chunks/3524.js +150 -67
- package/.next/standalone/.next/server/chunks/3566.js +1 -1
- package/.next/standalone/.next/server/chunks/4189.js +1 -1
- package/.next/standalone/.next/server/chunks/4596.js +28 -4
- package/.next/standalone/.next/server/chunks/46.js +91 -31
- package/.next/standalone/.next/server/chunks/466.js +53 -7
- package/.next/standalone/.next/server/chunks/4955.js +30 -3
- package/.next/standalone/.next/server/chunks/5585.js +28 -4
- package/.next/standalone/.next/server/chunks/5908.js +63 -42
- package/.next/standalone/.next/server/chunks/6142.js +10 -2
- package/.next/standalone/.next/server/chunks/8461.js +10 -8
- package/.next/standalone/.next/server/chunks/9032.js +5 -5
- package/.next/standalone/.next/server/chunks/9164.js +487 -177
- package/.next/standalone/.next/server/chunks/9304.js +51 -12
- package/.next/standalone/.next/server/chunks/{423.js → 9590.js} +84 -24
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +89 -21
- package/.next/standalone/.next/server/pages/404.html +14 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
- package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
- package/.next/standalone/.next/static/chunks/app/{page-213a623b37226452.js → page-2ef8d75d40eb4311.js} +10643 -10295
- package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
- package/.next/standalone/docs/adoption/changesets.json +2805 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +52 -0
- package/api/client.models.test.ts +10 -0
- package/api/client.startup.test.ts +129 -0
- package/api/client.ts +51 -9
- package/api/thread-metadata.test.ts +72 -0
- package/api/types.ts +6 -3
- package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
- package/app/api/v1/providers/[provider]/models/route.ts +259 -40
- package/app/api/v1/threads/[thread_id]/route.ts +26 -23
- package/app/api/v1/threads/[thread_id]/run/route.ts +0 -1
- package/app/globals.css +119 -29
- package/app/layout.tsx +12 -0
- package/components/agents/AgentsPanel.tsx +5 -13
- package/components/chat/ChatView.tsx +9 -18
- package/components/chat/InputBar.tsx +1 -1
- package/components/chat/MessageBubble.tsx +13 -1
- package/components/chat/MessageList.test.tsx +45 -4
- package/components/chat/MessageList.tsx +123 -89
- package/components/chat/ToolList.tsx +88 -84
- package/components/chat/useThreadActivity.test.tsx +2 -2
- package/components/chat/useThreadActivity.ts +2 -2
- package/components/chat/useThreadData.ts +1 -30
- package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
- package/components/chat/useThreadMetadataPolling.ts +50 -0
- package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
- package/components/chat/useUserProfileAndAgent.ts +13 -2
- package/components/dashboard/DashboardPanel.tsx +1 -1
- package/components/dashboard/Series.tsx +34 -21
- package/components/dashboard/SummaryCards.tsx +1 -1
- package/components/integrations/NetworkSection.tsx +2 -2
- package/components/layout/AppShell.tsx +22 -7
- package/components/layout/MenuPanel.tsx +14 -24
- package/components/profile/ProfilePanel.tsx +2 -2
- package/components/settings/AppearancePanel.tsx +70 -3
- package/components/settings/SettingsPanel.tsx +2 -2
- package/components/tools/ToolsPanel.tsx +2 -7
- package/components/ui/AppearancePreferences.test.tsx +68 -0
- package/components/ui/BootScreen.test.tsx +44 -0
- package/components/ui/BootScreen.tsx +24 -11
- package/components/ui/NotificationStatus.test.tsx +2 -2
- package/components/ui/NotificationStatus.tsx +5 -5
- package/components/ui/SubTabBar.tsx +2 -3
- package/contexts/ThemeContext.tsx +84 -1
- package/hooks/useAgentSession.test.ts +12 -1
- package/hooks/useAgentSession.ts +6 -4
- package/hooks/useUrlSync.test.ts +13 -0
- package/hooks/useUrlSync.ts +0 -4
- package/lib/agents/citation-checker.test.ts +40 -2
- package/lib/agents/citation-checker.ts +56 -11
- package/lib/agents/context-boundary.ts +5 -6
- package/lib/agents/instruction-edits.test.ts +120 -0
- package/lib/agents/instruction-edits.ts +65 -42
- package/lib/agents/prepare/request.ts +0 -2
- package/lib/agents/prepare/system-prompt.test.ts +10 -0
- package/lib/agents/prepare/system-prompt.ts +8 -2
- package/lib/agents/proposals.test.ts +76 -0
- package/lib/agents/proposals.ts +13 -5
- package/lib/agents/run-thread.auto-boundary.test.ts +125 -5
- package/lib/agents/run-thread.ts +24 -9
- package/lib/agents/sqlite-checkpoint-saver.test.ts +91 -0
- package/lib/agents/warm-summary-background.ts +22 -2
- package/lib/agents/warm-topics.test.ts +155 -1
- package/lib/agents/warm-topics.ts +27 -7
- package/lib/bridges/dispatcher.ts +1 -0
- package/lib/bridges/message-role.test.ts +81 -0
- package/lib/bridges/message-role.ts +23 -10
- package/lib/bridges/types.ts +6 -0
- package/lib/bridges/whatsapp-link.test.ts +32 -0
- package/lib/bridges/whatsapp-link.ts +21 -0
- package/lib/bridges/whatsapp.inbound.test.ts +239 -0
- package/lib/bridges/whatsapp.sender.test.ts +80 -1
- package/lib/bridges/whatsapp.ts +76 -38
- package/lib/db/migrations.test.ts +61 -0
- package/lib/db/migrations.ts +13 -0
- package/lib/embeddings/performance.test.ts +114 -0
- package/lib/pricing/snapshot.test.ts +70 -0
- package/lib/pricing/snapshot.ts +10 -2
- package/lib/proxy/dispatcher.test.ts +119 -1
- package/lib/proxy/dispatcher.ts +22 -9
- package/lib/stores/agent-configs.ts +10 -5
- package/lib/stores/bridges.test.ts +81 -0
- package/lib/stores/bridges.ts +9 -8
- package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
- package/lib/stores/dashboard-metrics.ts +30 -4
- package/lib/stores/message-embedding-jobs.test.ts +27 -0
- package/lib/stores/message-embedding-jobs.ts +5 -3
- package/lib/stores/message-usage.test.ts +21 -1
- package/lib/stores/pricing-cache.test.ts +31 -0
- package/lib/stores/pricing.ts +3 -0
- package/lib/stores/proxy-config.test.ts +120 -0
- package/lib/stores/proxy-config.ts +22 -4
- package/lib/stores/threads.ts +12 -8
- package/lib/tools/communications/calendar.test.ts +43 -2
- package/lib/tools/communications/calendar.ts +2 -0
- package/lib/tools/communications/gmail.test.ts +51 -1
- package/lib/tools/communications/gmail.ts +44 -2
- package/lib/tools/communications/outlook-calendar.ts +2 -0
- package/lib/tools/communications/outlook.test.ts +33 -2
- package/lib/tools/communications/outlook.ts +2 -0
- package/lib/tools/filesystem/edit-match.test.ts +133 -0
- package/lib/tools/filesystem/edit-match.ts +7 -2
- package/lib/tools/packages/default-packages.test.ts +21 -0
- package/lib/tools/packages/optional-package.test.ts +72 -0
- package/lib/tools/packages/optional-package.ts +33 -4
- package/lib/tools/support/result-refs.test.ts +131 -0
- package/lib/tools/support/result-refs.ts +41 -9
- package/lib/ui/navigate.test.ts +13 -0
- package/lib/ui/navigate.ts +10 -2
- package/lib/utils/source-link.test.ts +51 -0
- package/lib/utils/source-link.ts +45 -0
- package/package.json +1 -1
- package/.next/standalone/.next/static/css/312542a5725ba0d5.css +0 -5
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
|
@@ -92,7 +92,7 @@ export function SettingsPanel() {
|
|
|
92
92
|
}));
|
|
93
93
|
|
|
94
94
|
return (
|
|
95
|
-
<div className="
|
|
95
|
+
<div className="mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col">
|
|
96
96
|
<SubTabBar
|
|
97
97
|
ariaLabel="Settings sub-section"
|
|
98
98
|
tabs={tabItems}
|
|
@@ -120,7 +120,7 @@ export function SettingsPanel() {
|
|
|
120
120
|
// future change to either panel without touching this file.
|
|
121
121
|
function PrivacySecurityPanel() {
|
|
122
122
|
return (
|
|
123
|
-
<div className="h-full overflow-y-auto
|
|
123
|
+
<div className="h-full w-full overflow-y-auto px-4 py-3 no-scrollbar">
|
|
124
124
|
<SettingsGroup defaultOpenId="pin">
|
|
125
125
|
<SecurityPanel />
|
|
126
126
|
<RedactionPanel />
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { useAppContext } from "@/contexts/AppContext";
|
|
3
3
|
import { MCPPanel } from "@/components/mcp/MCPPanel";
|
|
4
4
|
import { SkillsPanel } from "@/components/skills/SkillsPanel";
|
|
5
|
-
import { UnifiedMemoryPanel } from "@/components/memory/UnifiedMemoryPanel";
|
|
6
5
|
import { BridgesPanel } from "@/components/bridges/BridgesPanel";
|
|
7
6
|
import { BrowserPanel } from "@/components/tools/BrowserPanel";
|
|
8
7
|
import { PackagesPanel } from "./PackagesPanel";
|
|
@@ -13,7 +12,7 @@ import { SubTabBar, type SubTabItem } from "@/components/ui/SubTabBar";
|
|
|
13
12
|
// manifests, and drop-in `.cjs` files. One home for
|
|
14
13
|
// everything that turns into a LangChain tool.
|
|
15
14
|
// - "Skills" — markdown playbooks (repos) the agent reads/writes on demand.
|
|
16
|
-
// -
|
|
15
|
+
// - Knowledge now has its own Advanced-menu destination.
|
|
17
16
|
// - "MCP" — external Model Context Protocol servers.
|
|
18
17
|
// - "Bridges" — mobile / messaging bridge pairings.
|
|
19
18
|
// - "Browser" — the local Chromium extension as an agent runtime surface.
|
|
@@ -23,17 +22,15 @@ type Sub =
|
|
|
23
22
|
| "packages"
|
|
24
23
|
| "browser"
|
|
25
24
|
| "skills"
|
|
26
|
-
| "memory"
|
|
27
25
|
| "mcp"
|
|
28
26
|
| "bridges";
|
|
29
27
|
|
|
30
|
-
const SUBS: Sub[] = ["packages", "browser", "skills", "
|
|
28
|
+
const SUBS: Sub[] = ["packages", "browser", "skills", "mcp", "bridges"];
|
|
31
29
|
|
|
32
30
|
const SUB_TITLES: Record<Sub, string> = {
|
|
33
31
|
packages: "Packages",
|
|
34
32
|
browser: "Browser",
|
|
35
33
|
skills: "Skills",
|
|
36
|
-
memory: "Memory",
|
|
37
34
|
mcp: "MCP servers",
|
|
38
35
|
bridges: "Bridges",
|
|
39
36
|
};
|
|
@@ -43,7 +40,6 @@ const SUB_TITLES: Record<Sub, string> = {
|
|
|
43
40
|
const LEGACY_SUBS: Record<string, Sub> = {
|
|
44
41
|
builtin: "packages",
|
|
45
42
|
extensions: "packages",
|
|
46
|
-
documents: "memory",
|
|
47
43
|
};
|
|
48
44
|
|
|
49
45
|
export function ToolsPanel() {
|
|
@@ -70,7 +66,6 @@ export function ToolsPanel() {
|
|
|
70
66
|
{active === "packages" && <PackagesPanel />}
|
|
71
67
|
{active === "browser" && <BrowserPanel />}
|
|
72
68
|
{active === "skills" && <SkillsPanel />}
|
|
73
|
-
{active === "memory" && <UnifiedMemoryPanel />}
|
|
74
69
|
{active === "mcp" && <MCPPanel />}
|
|
75
70
|
{active === "bridges" && <BridgesPanel />}
|
|
76
71
|
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { ThemeProvider, useTheme } from "@/contexts/ThemeContext";
|
|
5
|
+
|
|
6
|
+
function PreferenceProbe() {
|
|
7
|
+
const { accentColor, setAccentColor, surfaceStyle, setSurfaceStyle } = useTheme();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<span data-testid="accent">{accentColor}</span>
|
|
11
|
+
<span data-testid="surface">{surfaceStyle}</span>
|
|
12
|
+
<button onClick={() => setAccentColor("teal")}>Choose teal</button>
|
|
13
|
+
<button onClick={() => setAccentColor("default")}>Choose default</button>
|
|
14
|
+
<button onClick={() => setSurfaceStyle("ambient")}>Choose atmospheric</button>
|
|
15
|
+
<button onClick={() => setSurfaceStyle("liquid")}>Choose Liquid Glass</button>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("ThemeContext appearance preferences", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
localStorage.clear();
|
|
23
|
+
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({
|
|
24
|
+
matches: false,
|
|
25
|
+
addEventListener: vi.fn(),
|
|
26
|
+
removeEventListener: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.unstubAllGlobals();
|
|
32
|
+
document.documentElement.removeAttribute("data-accent-color");
|
|
33
|
+
document.documentElement.removeAttribute("data-surface-style");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("restores, applies, and resets appearance choices", async () => {
|
|
37
|
+
localStorage.setItem("jarela-accent-color", "rose");
|
|
38
|
+
localStorage.setItem("jarela-surface-style", "calm");
|
|
39
|
+
render(
|
|
40
|
+
<ThemeProvider>
|
|
41
|
+
<PreferenceProbe />
|
|
42
|
+
</ThemeProvider>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(screen.getByTestId("accent").textContent).toBe("rose");
|
|
47
|
+
expect(screen.getByTestId("surface").textContent).toBe("calm");
|
|
48
|
+
expect(document.documentElement.dataset.accentColor).toBe("rose");
|
|
49
|
+
expect(document.documentElement.dataset.surfaceStyle).toBe("calm");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose teal" }));
|
|
53
|
+
expect(document.documentElement.dataset.accentColor).toBe("teal");
|
|
54
|
+
expect(localStorage.getItem("jarela-accent-color")).toBe("teal");
|
|
55
|
+
|
|
56
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose default" }));
|
|
57
|
+
expect(document.documentElement.hasAttribute("data-accent-color")).toBe(false);
|
|
58
|
+
expect(localStorage.getItem("jarela-accent-color")).toBeNull();
|
|
59
|
+
|
|
60
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose atmospheric" }));
|
|
61
|
+
expect(document.documentElement.dataset.surfaceStyle).toBe("ambient");
|
|
62
|
+
expect(localStorage.getItem("jarela-surface-style")).toBe("ambient");
|
|
63
|
+
|
|
64
|
+
fireEvent.click(screen.getByRole("button", { name: "Choose Liquid Glass" }));
|
|
65
|
+
expect(document.documentElement.dataset.surfaceStyle).toBe("liquid");
|
|
66
|
+
expect(localStorage.getItem("jarela-surface-style")).toBe("liquid");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, render } from "@testing-library/react";
|
|
3
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
4
|
+
import { BootScreen } from "./BootScreen";
|
|
5
|
+
|
|
6
|
+
const reads = vi.hoisted(() => ({
|
|
7
|
+
profile: vi.fn(),
|
|
8
|
+
agent: vi.fn(),
|
|
9
|
+
thread: vi.fn(),
|
|
10
|
+
messages: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("@/api/client", () => ({
|
|
13
|
+
api: {
|
|
14
|
+
profile: { get: reads.profile },
|
|
15
|
+
agents: { get: reads.agent, getThread: reads.thread },
|
|
16
|
+
threads: { get: reads.messages },
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("@/components/ui/Logo", () => ({ Logo: () => null }));
|
|
20
|
+
vi.mock("@/components/ui/VersionTag", () => ({ VersionTag: () => null }));
|
|
21
|
+
vi.mock("@/components/ui/PoweredBy", () => ({ PoweredBy: () => null }));
|
|
22
|
+
vi.mock("@/components/ui/WorkflowChecklist", () => ({ WorkflowChecklist: () => null }));
|
|
23
|
+
|
|
24
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
25
|
+
|
|
26
|
+
it("does not restart boot prefetch after dismissal when the agent changes", async () => {
|
|
27
|
+
vi.useFakeTimers();
|
|
28
|
+
reads.profile.mockResolvedValue({});
|
|
29
|
+
reads.agent.mockResolvedValue({});
|
|
30
|
+
reads.thread.mockResolvedValue({ thread_id: "thread" });
|
|
31
|
+
reads.messages.mockResolvedValue({});
|
|
32
|
+
const onPickAgent = vi.fn();
|
|
33
|
+
const { rerender, container } = render(
|
|
34
|
+
<BootScreen agents={[]} agentsLoaded activeAgentId="first" onPickAgent={onPickAgent} />,
|
|
35
|
+
);
|
|
36
|
+
await act(async () => {});
|
|
37
|
+
await act(async () => { await vi.advanceTimersByTimeAsync(750); });
|
|
38
|
+
expect(container.innerHTML).toBe("");
|
|
39
|
+
rerender(<BootScreen agents={[]} agentsLoaded activeAgentId="second" onPickAgent={onPickAgent} />);
|
|
40
|
+
await act(async () => {});
|
|
41
|
+
expect(reads.agent).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(reads.agent).toHaveBeenCalledWith("first", { startup: true });
|
|
43
|
+
expect(reads.thread).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
@@ -61,6 +61,7 @@ interface Props {
|
|
|
61
61
|
agents: AgentConfig[];
|
|
62
62
|
agentsLoaded: boolean;
|
|
63
63
|
activeAgentId: string | null;
|
|
64
|
+
preferredThreadId?: string | null;
|
|
64
65
|
onPickAgent: (agentId: string) => void;
|
|
65
66
|
// Boot picker is a chat-tab gate; suppress on non-chat tabs so it doesn't overlay them.
|
|
66
67
|
suppressed?: boolean;
|
|
@@ -78,7 +79,7 @@ interface Props {
|
|
|
78
79
|
// Replaces the previous splash + ChatView empty-state agent picker. The
|
|
79
80
|
// header (z-40) is hidden while this is visible so the user lands on a
|
|
80
81
|
// focused, one-purpose screen.
|
|
81
|
-
export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, suppressed }: Props) {
|
|
82
|
+
export function BootScreen({ agents, agentsLoaded, activeAgentId, preferredThreadId, onPickAgent, suppressed }: Props) {
|
|
82
83
|
// Capture which agent we transitioned through so the icon can keep
|
|
83
84
|
// showing during the fade-out, even if a parent state shift would
|
|
84
85
|
// otherwise clear it.
|
|
@@ -96,7 +97,6 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
96
97
|
const [adoptionBusy, setAdoptionBusy] = useState<VersionAdoptionAction | null>(null);
|
|
97
98
|
const [adoptionLiveLabel, setAdoptionLiveLabel] = useState<string | null>(null);
|
|
98
99
|
const adoptionRef = useRef<VersionAdoptionState | null>(null);
|
|
99
|
-
const prefetchStartedRef = useRef(false);
|
|
100
100
|
const adoptionRunStartedRef = useRef<string | null>(null);
|
|
101
101
|
const adoptionAbortRef = useRef<AbortController | null>(null);
|
|
102
102
|
adoptionRef.current = adoption;
|
|
@@ -244,9 +244,8 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
244
244
|
// half-rendered chat that has to fetch the same data anyway.
|
|
245
245
|
useEffect(() => {
|
|
246
246
|
const id = activeAgentId ?? pickedId;
|
|
247
|
-
if (!id || !agentsLoaded) return;
|
|
248
|
-
|
|
249
|
-
prefetchStartedRef.current = true;
|
|
247
|
+
if (!id || !agentsLoaded || done) return;
|
|
248
|
+
setDismissing(false);
|
|
250
249
|
|
|
251
250
|
setSteps([
|
|
252
251
|
{ key: "profile", label: "loading your profile", status: "active" },
|
|
@@ -259,22 +258,36 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
259
258
|
|
|
260
259
|
void (async () => {
|
|
261
260
|
await Promise.allSettled([
|
|
262
|
-
api.profile.get().
|
|
261
|
+
api.profile.get({ startup: true }).catch((error: unknown) => {
|
|
262
|
+
console.error("startup profile warmup failed", error);
|
|
263
|
+
}).finally(() => {
|
|
263
264
|
if (!cancelled) markStep("profile", "done");
|
|
264
265
|
}),
|
|
265
266
|
api.agents
|
|
266
|
-
.get(id)
|
|
267
|
+
.get(id, { startup: true })
|
|
268
|
+
.catch((error: unknown) => {
|
|
269
|
+
console.error("startup agent warmup failed", error);
|
|
270
|
+
})
|
|
267
271
|
.finally(() => {
|
|
268
272
|
if (!cancelled) markStep("agent", "done");
|
|
269
273
|
}),
|
|
270
274
|
(async () => {
|
|
271
275
|
try {
|
|
272
|
-
|
|
276
|
+
if (preferredThreadId) {
|
|
277
|
+
try {
|
|
278
|
+
const preferred = await api.threads.get(preferredThreadId, { startup: true });
|
|
279
|
+
if (preferred.agent_id === id) return;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error("startup selected conversation warmup failed", error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const t = await api.agents.getThread(id, { startup: true });
|
|
273
285
|
if (cancelled) return;
|
|
274
286
|
markStep("thread", "done");
|
|
275
287
|
markStep("messages", "active");
|
|
276
|
-
await api.threads.get(t.thread_id);
|
|
277
|
-
} catch {
|
|
288
|
+
await api.threads.get(t.thread_id, { startup: true });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error("startup conversation warmup failed", error);
|
|
278
291
|
// Errors here are non-fatal for boot — ChatView will surface
|
|
279
292
|
// them with its own retry path. Just mark the steps done so
|
|
280
293
|
// we don't block the user behind the splash forever.
|
|
@@ -301,7 +314,7 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
|
|
|
301
314
|
cancelled = true;
|
|
302
315
|
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
|
|
303
316
|
};
|
|
304
|
-
}, [activeAgentId, pickedId, agentsLoaded, markStep]);
|
|
317
|
+
}, [activeAgentId, pickedId, preferredThreadId, agentsLoaded, markStep, done]);
|
|
305
318
|
|
|
306
319
|
if (done) return null;
|
|
307
320
|
if (suppressed) return null;
|
|
@@ -21,12 +21,12 @@ describe("NotificationStatus", () => {
|
|
|
21
21
|
it("renders a theme-safe denied state with an accessible dismiss action", async () => {
|
|
22
22
|
render(<NotificationStatus />);
|
|
23
23
|
|
|
24
|
-
const copy = await screen.findByText(/
|
|
24
|
+
const copy = await screen.findByText(/System notifications are blocked/);
|
|
25
25
|
const banner = copy.closest(".rounded-lg");
|
|
26
26
|
expect(banner?.className).toContain("bg-rose-500/10");
|
|
27
27
|
expect(screen.getByRole("button", { name: "Dismiss notification prompt" })).toBeTruthy();
|
|
28
28
|
|
|
29
29
|
fireEvent.click(screen.getByRole("button", { name: "Dismiss notification prompt" }));
|
|
30
|
-
expect(screen.queryByText(/
|
|
30
|
+
expect(screen.queryByText(/System notifications are blocked/)).toBeNull();
|
|
31
31
|
});
|
|
32
32
|
});
|
|
@@ -85,15 +85,15 @@ export function NotificationStatus() {
|
|
|
85
85
|
: "border-amber-500/30 bg-amber-500/10";
|
|
86
86
|
|
|
87
87
|
return (
|
|
88
|
-
<div className="absolute top-9 left-0 right-0 z-30 mx-auto max-w-
|
|
89
|
-
<div role="status" className={`pointer-events-auto rounded-lg border ${bannerTone} backdrop-blur px-3 py-2 flex items-center gap-2 shadow-lg`}>
|
|
88
|
+
<div className="absolute top-9 left-0 right-0 z-30 mx-auto max-w-xl px-4 mt-2 pointer-events-none">
|
|
89
|
+
<div role="status" className={`pointer-events-auto rounded-lg border ${bannerTone} backdrop-blur px-3 py-2 flex flex-wrap items-center gap-x-2 gap-y-1 shadow-lg`}>
|
|
90
90
|
{status === "denied"
|
|
91
91
|
? <BellOff size={14} className="text-rose-700 dark:text-rose-400 shrink-0" />
|
|
92
92
|
: <Bell size={14} className="text-amber-700 dark:text-amber-400 shrink-0" />}
|
|
93
|
-
<p className="text-xs text-fg
|
|
93
|
+
<p className="min-w-0 flex-1 basis-full text-xs text-fg sm:basis-auto">
|
|
94
94
|
{status === "denied"
|
|
95
|
-
? "
|
|
96
|
-
: "Enable
|
|
95
|
+
? "System notifications are blocked. In-app alerts still work; change this in browser site settings."
|
|
96
|
+
: "Enable system alerts for when Jarela isn't focused."}
|
|
97
97
|
</p>
|
|
98
98
|
{status === "default" && (
|
|
99
99
|
<button
|
|
@@ -159,11 +159,10 @@ function EdgeArrow({ side, onClick }: { side: "left" | "right"; onClick: () => v
|
|
|
159
159
|
return (
|
|
160
160
|
<button
|
|
161
161
|
type="button"
|
|
162
|
-
|
|
163
|
-
aria-hidden="true"
|
|
162
|
+
aria-label={`Scroll tabs ${side}`}
|
|
164
163
|
onClick={onClick}
|
|
165
164
|
className={
|
|
166
|
-
"absolute inset-y-0 w-8 flex items-center
|
|
165
|
+
"absolute inset-y-0 w-8 flex items-center text-fg-muted hover:text-fg from-[var(--bg-secondary)] via-[var(--bg-secondary)] to-transparent " +
|
|
167
166
|
(side === "left"
|
|
168
167
|
? "bg-gradient-to-r left-0 justify-start pl-1"
|
|
169
168
|
: "bg-gradient-to-l right-0 justify-end pr-1")
|
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
export type Theme = "light" | "dark" | "system";
|
|
5
|
+
export type AccentColor = "default" | "blue" | "teal" | "violet" | "rose";
|
|
6
|
+
export type SurfaceStyle = "ambient" | "calm" | "liquid";
|
|
5
7
|
|
|
6
8
|
const STORAGE_KEY = "jarela-theme";
|
|
9
|
+
const ACCENT_STORAGE_KEY = "jarela-accent-color";
|
|
10
|
+
const SURFACE_STORAGE_KEY = "jarela-surface-style";
|
|
7
11
|
|
|
8
12
|
function isTheme(v: unknown): v is Theme {
|
|
9
13
|
return v === "light" || v === "dark" || v === "system";
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
function isAccentColor(v: unknown): v is AccentColor {
|
|
17
|
+
return v === "default" || v === "blue" || v === "teal" || v === "violet" || v === "rose";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isSurfaceStyle(v: unknown): v is SurfaceStyle {
|
|
21
|
+
return v === "ambient" || v === "calm" || v === "liquid";
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
function readStored(): Theme {
|
|
13
25
|
if (typeof window === "undefined") return "system";
|
|
14
26
|
try {
|
|
@@ -19,6 +31,26 @@ function readStored(): Theme {
|
|
|
19
31
|
}
|
|
20
32
|
}
|
|
21
33
|
|
|
34
|
+
function readStoredAccent(): AccentColor {
|
|
35
|
+
if (typeof window === "undefined") return "blue";
|
|
36
|
+
try {
|
|
37
|
+
const value = window.localStorage.getItem(ACCENT_STORAGE_KEY);
|
|
38
|
+
return isAccentColor(value) ? value : "default";
|
|
39
|
+
} catch {
|
|
40
|
+
return "default";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readStoredSurface(): SurfaceStyle {
|
|
45
|
+
if (typeof window === "undefined") return "ambient";
|
|
46
|
+
try {
|
|
47
|
+
const value = window.localStorage.getItem(SURFACE_STORAGE_KEY);
|
|
48
|
+
return isSurfaceStyle(value) ? value : "ambient";
|
|
49
|
+
} catch {
|
|
50
|
+
return "ambient";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
22
54
|
const LIGHT_CHROME = "#ffffff";
|
|
23
55
|
const DARK_CHROME = "#09090b";
|
|
24
56
|
|
|
@@ -44,9 +76,27 @@ function apply(theme: Theme) {
|
|
|
44
76
|
syncChrome(theme);
|
|
45
77
|
}
|
|
46
78
|
|
|
79
|
+
function applyAccentColor(accentColor: AccentColor) {
|
|
80
|
+
if (typeof document === "undefined") return;
|
|
81
|
+
if (accentColor === "default") {
|
|
82
|
+
document.documentElement.removeAttribute("data-accent-color");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
document.documentElement.setAttribute("data-accent-color", accentColor);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applySurfaceStyle(surfaceStyle: SurfaceStyle) {
|
|
89
|
+
if (typeof document === "undefined") return;
|
|
90
|
+
document.documentElement.setAttribute("data-surface-style", surfaceStyle);
|
|
91
|
+
}
|
|
92
|
+
|
|
47
93
|
interface Ctx {
|
|
48
94
|
theme: Theme;
|
|
49
95
|
setTheme: (t: Theme) => void;
|
|
96
|
+
accentColor: AccentColor;
|
|
97
|
+
setAccentColor: (color: AccentColor) => void;
|
|
98
|
+
surfaceStyle: SurfaceStyle;
|
|
99
|
+
setSurfaceStyle: (style: SurfaceStyle) => void;
|
|
50
100
|
}
|
|
51
101
|
|
|
52
102
|
const ThemeContext = createContext<Ctx | null>(null);
|
|
@@ -56,10 +106,18 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
56
106
|
// which writes data-theme before React hydrates. The effect below syncs the
|
|
57
107
|
// React state to whatever the script (or localStorage) decided.
|
|
58
108
|
const [theme, setThemeState] = useState<Theme>("system");
|
|
109
|
+
const [accentColor, setAccentColorState] = useState<AccentColor>("blue");
|
|
110
|
+
const [surfaceStyle, setSurfaceStyleState] = useState<SurfaceStyle>("ambient");
|
|
59
111
|
|
|
60
112
|
useEffect(() => {
|
|
61
113
|
const stored = readStored();
|
|
114
|
+
const storedAccent = readStoredAccent();
|
|
115
|
+
const storedSurface = readStoredSurface();
|
|
62
116
|
setThemeState(stored);
|
|
117
|
+
setAccentColorState(storedAccent);
|
|
118
|
+
setSurfaceStyleState(storedSurface);
|
|
119
|
+
applyAccentColor(storedAccent);
|
|
120
|
+
applySurfaceStyle(storedSurface);
|
|
63
121
|
// When in "system" mode, mirror OS-level changes into the PWA chrome.
|
|
64
122
|
if (typeof window === "undefined") return;
|
|
65
123
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
@@ -80,7 +138,32 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
80
138
|
}
|
|
81
139
|
}, []);
|
|
82
140
|
|
|
83
|
-
|
|
141
|
+
const setAccentColor = useCallback((color: AccentColor) => {
|
|
142
|
+
setAccentColorState(color);
|
|
143
|
+
applyAccentColor(color);
|
|
144
|
+
try {
|
|
145
|
+
if (color === "default") window.localStorage.removeItem(ACCENT_STORAGE_KEY);
|
|
146
|
+
else window.localStorage.setItem(ACCENT_STORAGE_KEY, color);
|
|
147
|
+
} catch {
|
|
148
|
+
/* ignore quota / private-mode errors */
|
|
149
|
+
}
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
const setSurfaceStyle = useCallback((style: SurfaceStyle) => {
|
|
153
|
+
setSurfaceStyleState(style);
|
|
154
|
+
applySurfaceStyle(style);
|
|
155
|
+
try {
|
|
156
|
+
window.localStorage.setItem(SURFACE_STORAGE_KEY, style);
|
|
157
|
+
} catch {
|
|
158
|
+
/* ignore quota / private-mode errors */
|
|
159
|
+
}
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<ThemeContext.Provider value={{ theme, setTheme, accentColor, setAccentColor, surfaceStyle, setSurfaceStyle }}>
|
|
164
|
+
{children}
|
|
165
|
+
</ThemeContext.Provider>
|
|
166
|
+
);
|
|
84
167
|
}
|
|
85
168
|
|
|
86
169
|
export function useTheme() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { useAgentSession } from "@/hooks/useAgentSession";
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ vi.mock("@/api/client", () => ({
|
|
|
11
11
|
api: {
|
|
12
12
|
threads: {
|
|
13
13
|
get: (...args: unknown[]) => threadGetMock(...args),
|
|
14
|
+
metadata: (...args: unknown[]) => threadGetMock(...args),
|
|
14
15
|
},
|
|
15
16
|
agents: {
|
|
16
17
|
getThread: (...args: unknown[]) => agentGetThreadMock(...args),
|
|
@@ -35,5 +36,15 @@ describe("useAgentSession contract", () => {
|
|
|
35
36
|
expect(result.current.loading).toBe(result.current.state.loading);
|
|
36
37
|
expect(result.current.refresh).toBe(result.current.commands.refresh);
|
|
37
38
|
expect(agentGetThreadMock).not.toHaveBeenCalled();
|
|
39
|
+
expect(threadGetMock).toHaveBeenCalledWith("preferred-thread", { startup: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses the startup handoff initially but keeps explicit refreshes fresh", async () => {
|
|
43
|
+
agentGetThreadMock.mockResolvedValue({ thread_id: "thread" });
|
|
44
|
+
const { result } = renderHook(() => useAgentSession("agent"));
|
|
45
|
+
await waitFor(() => expect(result.current.threadId).toBe("thread"));
|
|
46
|
+
expect(agentGetThreadMock).toHaveBeenLastCalledWith("agent", { startup: true });
|
|
47
|
+
await act(async () => { await result.current.refresh(); });
|
|
48
|
+
expect(agentGetThreadMock).toHaveBeenLastCalledWith("agent", undefined);
|
|
38
49
|
});
|
|
39
50
|
});
|
package/hooks/useAgentSession.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function useAgentSession(
|
|
|
15
15
|
const [error, setError] = useState<string | null>(null);
|
|
16
16
|
const requestSeqRef = useRef(0);
|
|
17
17
|
|
|
18
|
-
const refresh = useCallback(async () => {
|
|
18
|
+
const refresh = useCallback(async (opts?: { startup?: boolean }) => {
|
|
19
19
|
if (!agentId) {
|
|
20
20
|
setThreadId(null);
|
|
21
21
|
setError(null);
|
|
@@ -32,7 +32,9 @@ export function useAgentSession(
|
|
|
32
32
|
// prefer it as long as it still belongs to this agent.
|
|
33
33
|
if (preferredThreadId) {
|
|
34
34
|
try {
|
|
35
|
-
const d =
|
|
35
|
+
const d = opts?.startup
|
|
36
|
+
? await api.threads.get(preferredThreadId, { startup: true })
|
|
37
|
+
: await api.threads.metadata(preferredThreadId);
|
|
36
38
|
if (mySeq !== requestSeqRef.current) return;
|
|
37
39
|
if (d.agent_id === agentId) {
|
|
38
40
|
setThreadId(preferredThreadId);
|
|
@@ -43,7 +45,7 @@ export function useAgentSession(
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
const t = await api.agents.getThread(agentId);
|
|
48
|
+
const t = await api.agents.getThread(agentId, opts);
|
|
47
49
|
if (mySeq !== requestSeqRef.current) return;
|
|
48
50
|
setThreadId(t.thread_id);
|
|
49
51
|
} catch (err) {
|
|
@@ -70,7 +72,7 @@ export function useAgentSession(
|
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
void refresh();
|
|
75
|
+
void refresh({ startup: true });
|
|
74
76
|
|
|
75
77
|
}, [agentId, preferredThreadId, refresh]);
|
|
76
78
|
|
package/hooks/useUrlSync.test.ts
CHANGED
|
@@ -30,4 +30,17 @@ describe("useUrlSync contract", () => {
|
|
|
30
30
|
replaceStateSpy.mockRestore();
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it.each([
|
|
35
|
+
"/?tab=documents",
|
|
36
|
+
"/?tab=tools&item=documents",
|
|
37
|
+
"/?tab=tools&item=memory",
|
|
38
|
+
])("routes legacy Knowledge URL %s to the Advanced Knowledge panel", async (url) => {
|
|
39
|
+
window.history.replaceState(null, "", url);
|
|
40
|
+
renderHook(() => useUrlSync(), { wrapper });
|
|
41
|
+
|
|
42
|
+
await waitFor(() => {
|
|
43
|
+
expect(window.location.search).toBe("?tab=memory");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
33
46
|
});
|
package/hooks/useUrlSync.ts
CHANGED
|
@@ -38,10 +38,6 @@ export function useUrlSync(): UnifiedHookResult<
|
|
|
38
38
|
tab = "tools";
|
|
39
39
|
item = "packages";
|
|
40
40
|
}
|
|
41
|
-
if (tab === "documents" || tab === "memory") {
|
|
42
|
-
tab = "tools";
|
|
43
|
-
item = "memory";
|
|
44
|
-
}
|
|
45
41
|
// Browser-extension "Open Jarela" passes ?agent=<id> (and optionally
|
|
46
42
|
// ?thread=<id>) so the app lands on the user's currently-picked
|
|
47
43
|
// target agent instead of whichever chat was last open. The agent
|
|
@@ -27,11 +27,27 @@ afterAll(() => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
describe("normalizeSource", () => {
|
|
30
|
-
it("lowercases + strips
|
|
31
|
-
expect(normalizeSource("HTTPS://Example.com/Docs/?x=
|
|
30
|
+
it("lowercases + strips tracking params, page anchors and trailing slash on URLs", () => {
|
|
31
|
+
expect(normalizeSource("HTTPS://Example.com/Docs/?utm_source=x&fbclid=y#a")).toBe("https://example.com/docs");
|
|
32
32
|
expect(normalizeSource("https://a.io/p/")).toBe("https://a.io/p");
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
it("keeps the query and hash route that identify a message permalink", () => {
|
|
36
|
+
const a = normalizeSource("https://outlook.office365.com/owa/?ItemID=AAMk%2BA%3D&exvsurl=1&viewmodel=ReadMessageItem");
|
|
37
|
+
const b = normalizeSource("https://outlook.office365.com/owa/?ItemID=AAMk%2BB%3D&exvsurl=1&viewmodel=ReadMessageItem");
|
|
38
|
+
expect(a).not.toBe(b);
|
|
39
|
+
// Base64 item ids are case-sensitive; folding case would merge distinct messages.
|
|
40
|
+
expect(a).toContain("ItemID=AAMk%2BA%3D");
|
|
41
|
+
expect(normalizeSource("https://mail.google.com/mail/u/0/#all/18c2f0a1b2c3d4e5"))
|
|
42
|
+
.toBe("https://mail.google.com/mail/u/0#all/18c2f0a1b2c3d4e5");
|
|
43
|
+
expect(normalizeSource("https://www.youtube.com/watch?v=dQw4w9WgXcQ&utm_medium=share"))
|
|
44
|
+
.toBe("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("treats the same permalink with reordered params as one source", () => {
|
|
48
|
+
expect(normalizeSource("https://x.test/r?b=2&a=1")).toBe(normalizeSource("https://x.test/r?a=1&b=2&utm_campaign=z"));
|
|
49
|
+
});
|
|
50
|
+
|
|
35
51
|
it("normalises backslashes and trailing slashes on paths", () => {
|
|
36
52
|
expect(normalizeSource("docs\\Adr\\README.md")).toBe("docs/adr/readme.md");
|
|
37
53
|
expect(normalizeSource("lib/agents/")).toBe("lib/agents");
|
|
@@ -137,6 +153,18 @@ describe("parseCitationVerdict", () => {
|
|
|
137
153
|
expect(out).toEqual([{ text: "a", marker: null, link: null, verified: false, reason: "no link", impact: "med" }]);
|
|
138
154
|
});
|
|
139
155
|
|
|
156
|
+
it("finds the verdict when the model wraps it in prose that also contains braces", () => {
|
|
157
|
+
const verdict = '{"claims":[{"text":"rate is 5%","marker":1,"verified":true,"reason":"in [1]","impact":"high"}]}';
|
|
158
|
+
const expected = [{ text: "rate is 5%", marker: 1, link: null, verified: true, reason: "in [1]", impact: "high" }];
|
|
159
|
+
expect(parseCitationVerdict(`Checked the {draft} reply. ${verdict}`)).toEqual(expected);
|
|
160
|
+
expect(parseCitationVerdict(`${verdict}\nNote: ignored the {code} block.`)).toEqual(expected);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("keeps braces that belong to a claim string", () => {
|
|
164
|
+
const out = parseCitationVerdict('{"claims":[{"text":"set {a: 1} in config","verified":false,"reason":"} unmatched"}]}');
|
|
165
|
+
expect(out?.[0].text).toBe("set {a: 1} in config");
|
|
166
|
+
expect(out?.[0].reason).toBe("} unmatched");
|
|
167
|
+
});
|
|
140
168
|
it("returns null on malformed JSON", () => {
|
|
141
169
|
expect(parseCitationVerdict("{not json")).toBeNull();
|
|
142
170
|
});
|
|
@@ -299,6 +327,16 @@ describe("mergeDeclaredReferences", () => {
|
|
|
299
327
|
]);
|
|
300
328
|
});
|
|
301
329
|
|
|
330
|
+
it("keeps two declared emails that differ only by their permalink id", () => {
|
|
331
|
+
const out = mergeDeclaredReferences([], [
|
|
332
|
+
{ label: "Invoice mail", href: "https://mail.google.com/mail/u/0/#all/18c2f0a1b2c3d4e5" },
|
|
333
|
+
{ label: "Contract mail", href: "https://mail.google.com/mail/u/0/#all/18c2f0a1b2c3ffff" },
|
|
334
|
+
{ label: "Board invite", href: "https://outlook.office365.com/owa/?ItemID=AAMk%2BA%3D&exvsurl=1" },
|
|
335
|
+
{ label: "Offsite invite", href: "https://outlook.office365.com/owa/?ItemID=AAMk%2BB%3D&exvsurl=1" },
|
|
336
|
+
]);
|
|
337
|
+
expect(out.map((e) => e.label)).toEqual(["Invoice mail", "Contract mail", "Board invite", "Offsite invite"]);
|
|
338
|
+
});
|
|
339
|
+
|
|
302
340
|
it("returns a copy of the manifest unchanged when there are no declared refs", () => {
|
|
303
341
|
const manifest = [{ n: 1, label: "x", href: "x" }];
|
|
304
342
|
expect(mergeDeclaredReferences(manifest, [])).toEqual(manifest);
|