@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({ metadata: vi.fn() }));
|
|
6
|
+
vi.mock("@/api/client", () => ({ api: { threads: { metadata: mocks.metadata } } }));
|
|
7
|
+
const { useThreadMetadataPolling } = await import("./useThreadMetadataPolling");
|
|
8
|
+
|
|
9
|
+
function applier() {
|
|
10
|
+
return {
|
|
11
|
+
setHotSince: vi.fn(), setHotSinceSeq: vi.fn(), setWarmSummary: vi.fn(),
|
|
12
|
+
setWarmSummaryBefore: vi.fn(), setWarmSummaryBeforeSeq: vi.fn(),
|
|
13
|
+
setWarmSummaryComputedAt: vi.fn(), setWarmSummarySourceMessages: vi.fn(),
|
|
14
|
+
setWarmSummarySourceChars: vi.fn(), setContextWindowTokens: vi.fn(),
|
|
15
|
+
setWarmSummaryPending: vi.fn(), setCompactionPending: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function advance(ms: number) {
|
|
20
|
+
await act(async () => { await vi.advanceTimersByTimeAsync(ms); });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("thread metadata polling", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.useFakeTimers();
|
|
26
|
+
mocks.metadata.mockReset().mockResolvedValue({ hot_since_seq: 4, warm_summary_before_seq: 4 });
|
|
27
|
+
Object.defineProperty(document, "hidden", { configurable: true, value: false });
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.useRealTimers();
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
Reflect.deleteProperty(document, "hidden");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("uses metadata reads and stops when disabled", async () => {
|
|
36
|
+
const meta = applier();
|
|
37
|
+
const { rerender } = renderHook(({ enabled }) => useThreadMetadataPolling("t1", enabled, meta),
|
|
38
|
+
{ initialProps: { enabled: true } });
|
|
39
|
+
await advance(1600);
|
|
40
|
+
expect(mocks.metadata).toHaveBeenCalledWith("t1", expect.any(AbortSignal));
|
|
41
|
+
expect(meta.setHotSinceSeq).toHaveBeenCalledWith(4);
|
|
42
|
+
expect(meta.setWarmSummaryPending).toHaveBeenCalledWith(false);
|
|
43
|
+
rerender({ enabled: false });
|
|
44
|
+
await advance(5000);
|
|
45
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("waits for each response before scheduling another read", async () => {
|
|
49
|
+
let resolve!: (value: object) => void;
|
|
50
|
+
mocks.metadata.mockReturnValueOnce(new Promise((r) => { resolve = r; }));
|
|
51
|
+
renderHook(() => useThreadMetadataPolling("t1", true, applier()));
|
|
52
|
+
await advance(8000);
|
|
53
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(1);
|
|
54
|
+
await act(async () => resolve({}));
|
|
55
|
+
await advance(1599);
|
|
56
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(1);
|
|
57
|
+
await advance(1);
|
|
58
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("pauses while hidden and refreshes immediately on visibility", async () => {
|
|
62
|
+
Object.defineProperty(document, "hidden", { configurable: true, value: true });
|
|
63
|
+
renderHook(() => useThreadMetadataPolling("t1", true, applier()));
|
|
64
|
+
await advance(5000);
|
|
65
|
+
expect(mocks.metadata).not.toHaveBeenCalled();
|
|
66
|
+
Object.defineProperty(document, "hidden", { configurable: true, value: false });
|
|
67
|
+
await act(async () => document.dispatchEvent(new Event("visibilitychange")));
|
|
68
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not overlap an in-flight read when the page becomes visible again", async () => {
|
|
72
|
+
let resolve!: (value: object) => void;
|
|
73
|
+
mocks.metadata.mockReturnValueOnce(new Promise((r) => { resolve = r; }));
|
|
74
|
+
const { unmount } = renderHook(() => useThreadMetadataPolling("t1", true, applier()));
|
|
75
|
+
await advance(1600);
|
|
76
|
+
Object.defineProperty(document, "hidden", { configurable: true, value: true });
|
|
77
|
+
await act(async () => document.dispatchEvent(new Event("visibilitychange")));
|
|
78
|
+
Object.defineProperty(document, "hidden", { configurable: true, value: false });
|
|
79
|
+
await act(async () => document.dispatchEvent(new Event("visibilitychange")));
|
|
80
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(1);
|
|
81
|
+
await act(async () => resolve({}));
|
|
82
|
+
await advance(1600);
|
|
83
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(2);
|
|
84
|
+
unmount();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("uses the latest metadata setters without restarting the polling timer", async () => {
|
|
88
|
+
const first = applier();
|
|
89
|
+
const second = applier();
|
|
90
|
+
const { rerender } = renderHook(({ meta }) => useThreadMetadataPolling("t1", true, meta),
|
|
91
|
+
{ initialProps: { meta: first } });
|
|
92
|
+
await advance(800);
|
|
93
|
+
rerender({ meta: second });
|
|
94
|
+
await advance(800);
|
|
95
|
+
expect(mocks.metadata).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(first.setHotSinceSeq).not.toHaveBeenCalled();
|
|
97
|
+
expect(second.setHotSinceSeq).toHaveBeenCalledWith(4);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("aborts and ignores stale responses after switching threads", async () => {
|
|
101
|
+
let resolve!: (value: object) => void;
|
|
102
|
+
mocks.metadata.mockReturnValueOnce(new Promise((r) => { resolve = r; }));
|
|
103
|
+
const meta = applier();
|
|
104
|
+
const { rerender } = renderHook(({ id }) => useThreadMetadataPolling(id, true, meta),
|
|
105
|
+
{ initialProps: { id: "t1" } });
|
|
106
|
+
await advance(1600);
|
|
107
|
+
const signal = mocks.metadata.mock.calls[0][1] as AbortSignal;
|
|
108
|
+
rerender({ id: "t2" });
|
|
109
|
+
expect(signal.aborted).toBe(true);
|
|
110
|
+
await act(async () => resolve({ hot_since_seq: 99 }));
|
|
111
|
+
expect(meta.setHotSinceSeq).not.toHaveBeenCalled();
|
|
112
|
+
await advance(1600);
|
|
113
|
+
expect(mocks.metadata).toHaveBeenLastCalledWith("t2", expect.any(AbortSignal));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("logs failures and retries without replacing metadata with defaults", async () => {
|
|
117
|
+
const error = new Error("offline");
|
|
118
|
+
mocks.metadata.mockRejectedValueOnce(error);
|
|
119
|
+
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
120
|
+
const meta = applier();
|
|
121
|
+
renderHook(() => useThreadMetadataPolling("t1", true, meta));
|
|
122
|
+
await advance(1600);
|
|
123
|
+
expect(log).toHaveBeenCalledWith("thread metadata refresh failed", error);
|
|
124
|
+
expect(meta.setHotSinceSeq).not.toHaveBeenCalled();
|
|
125
|
+
await advance(1600);
|
|
126
|
+
expect(meta.setHotSinceSeq).toHaveBeenCalledWith(4);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { api } from "@/api/client";
|
|
4
|
+
import { applyThreadMeta, type ThreadMetaApplier } from "./chat-helpers";
|
|
5
|
+
|
|
6
|
+
export function useThreadMetadataPolling(
|
|
7
|
+
threadId: string | null,
|
|
8
|
+
enabled: boolean,
|
|
9
|
+
metaApplier: ThreadMetaApplier,
|
|
10
|
+
) {
|
|
11
|
+
const metaRef = useRef(metaApplier);
|
|
12
|
+
metaRef.current = metaApplier;
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!threadId || !enabled) return;
|
|
16
|
+
const targetThreadId = threadId;
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
let inFlight = false;
|
|
19
|
+
let timer: number | undefined;
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const schedule = () => {
|
|
22
|
+
if (!cancelled && !document.hidden) timer = window.setTimeout(tick, 1600);
|
|
23
|
+
};
|
|
24
|
+
async function tick() {
|
|
25
|
+
if (cancelled || inFlight || document.hidden) return;
|
|
26
|
+
inFlight = true;
|
|
27
|
+
try {
|
|
28
|
+
const metadata = await api.threads.metadata(targetThreadId, controller.signal);
|
|
29
|
+
if (!cancelled) applyThreadMeta(metaRef.current, metadata);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (!cancelled) console.error("thread metadata refresh failed", err);
|
|
32
|
+
} finally {
|
|
33
|
+
inFlight = false;
|
|
34
|
+
schedule();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const onVisibilityChange = () => {
|
|
38
|
+
window.clearTimeout(timer);
|
|
39
|
+
if (!document.hidden) void tick();
|
|
40
|
+
};
|
|
41
|
+
schedule();
|
|
42
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
43
|
+
return () => {
|
|
44
|
+
cancelled = true;
|
|
45
|
+
controller.abort();
|
|
46
|
+
window.clearTimeout(timer);
|
|
47
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
48
|
+
};
|
|
49
|
+
}, [threadId, enabled]);
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import { expect, it, vi } from "vitest";
|
|
4
|
+
import { useUserProfileAndAgent } from "./useUserProfileAndAgent";
|
|
5
|
+
|
|
6
|
+
const { getAgent, getProfile } = vi.hoisted(() => ({
|
|
7
|
+
getAgent: vi.fn(),
|
|
8
|
+
getProfile: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("@/api/client", () => ({
|
|
11
|
+
api: { agents: { get: getAgent }, profile: { get: getProfile } },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
it("warms startup reads and ignores an agent response after switching agents", async () => {
|
|
15
|
+
let resolveOld!: (config: { id: string }) => void;
|
|
16
|
+
getProfile.mockResolvedValue({ name: "Test user" });
|
|
17
|
+
getAgent.mockImplementation((id: string) => id === "old"
|
|
18
|
+
? new Promise((resolve) => { resolveOld = resolve; })
|
|
19
|
+
: Promise.resolve({ id }));
|
|
20
|
+
const { result, rerender } = renderHook(
|
|
21
|
+
({ id }) => useUserProfileAndAgent(id),
|
|
22
|
+
{ initialProps: { id: "old" } },
|
|
23
|
+
);
|
|
24
|
+
rerender({ id: "new" });
|
|
25
|
+
await waitFor(() => expect(result.current.agentConfig?.id).toBe("new"));
|
|
26
|
+
await act(async () => resolveOld({ id: "old" }));
|
|
27
|
+
expect(result.current.agentConfig?.id).toBe("new");
|
|
28
|
+
expect(result.current.agentConfigLoading).toBe(false);
|
|
29
|
+
expect(getProfile).toHaveBeenCalledWith({ startup: true });
|
|
30
|
+
expect(getAgent).toHaveBeenCalledWith("new", { startup: true });
|
|
31
|
+
});
|
|
@@ -18,7 +18,12 @@ export function useUserProfileAndAgent(agentId: string | null): Result {
|
|
|
18
18
|
|
|
19
19
|
useEffect(() => {
|
|
20
20
|
setProfileLoading(true);
|
|
21
|
-
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
api.profile.get({ startup: true })
|
|
23
|
+
.then((profile) => { if (!cancelled) setUserProfile(profile); })
|
|
24
|
+
.catch((error) => { if (!cancelled) console.error(error); })
|
|
25
|
+
.finally(() => { if (!cancelled) setProfileLoading(false); });
|
|
26
|
+
return () => { cancelled = true; };
|
|
22
27
|
}, []);
|
|
23
28
|
|
|
24
29
|
useEffect(() => {
|
|
@@ -27,8 +32,14 @@ export function useUserProfileAndAgent(agentId: string | null): Result {
|
|
|
27
32
|
setAgentConfigLoading(false);
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
setAgentConfig(null);
|
|
30
37
|
setAgentConfigLoading(true);
|
|
31
|
-
api.agents.get(agentId
|
|
38
|
+
api.agents.get(agentId, { startup: true })
|
|
39
|
+
.then((config) => { if (!cancelled) setAgentConfig(config); })
|
|
40
|
+
.catch((error) => { if (!cancelled) console.error(error); })
|
|
41
|
+
.finally(() => { if (!cancelled) setAgentConfigLoading(false); });
|
|
42
|
+
return () => { cancelled = true; };
|
|
32
43
|
}, [agentId]);
|
|
33
44
|
|
|
34
45
|
return { userProfile, profileLoading, agentConfig, agentConfigLoading };
|
|
@@ -54,7 +54,7 @@ export function DashboardPanel() {
|
|
|
54
54
|
return (
|
|
55
55
|
<div className="relative h-full overflow-y-auto p-4 md:p-6 space-y-4">
|
|
56
56
|
<div className="mx-auto max-w-7xl space-y-4">
|
|
57
|
-
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
57
|
+
<div className="dashboard-atmosphere pointer-events-none absolute inset-0 overflow-hidden">
|
|
58
58
|
<div className="absolute -top-20 -right-20 w-72 h-72 rounded-full bg-cyan-500/10 blur-3xl" />
|
|
59
59
|
<div className="absolute top-64 -left-24 w-80 h-80 rounded-full bg-emerald-500/10 blur-3xl" />
|
|
60
60
|
</div>
|
|
@@ -15,6 +15,7 @@ export function InteractiveTokenChart({ series, selectedDay, onSelectDay }: Toke
|
|
|
15
15
|
const [barsReady, setBarsReady] = useState(false);
|
|
16
16
|
const maxTotal = series.reduce((max, p) => Math.max(max, p.input_tokens_est + p.output_tokens_est), 0);
|
|
17
17
|
const active = hovered != null ? series[hovered] : null;
|
|
18
|
+
const labelInterval = Math.max(1, Math.ceil(series.length / 7));
|
|
18
19
|
const animationSignature = series.map((s) => `${s.day}:${s.input_tokens_est}:${s.output_tokens_est}`).join("|");
|
|
19
20
|
|
|
20
21
|
useEffect(() => {
|
|
@@ -30,29 +31,35 @@ export function InteractiveTokenChart({ series, selectedDay, onSelectDay }: Toke
|
|
|
30
31
|
return (
|
|
31
32
|
<div>
|
|
32
33
|
<div className="relative h-48 rounded-xl bg-[var(--bg-primary)]/35 p-2">
|
|
34
|
+
<div className="h-full overflow-x-auto overscroll-x-contain">
|
|
35
|
+
<div className="h-full flex items-end gap-1 pt-5" style={{ minWidth: `${series.length * 18}px` }}>
|
|
36
|
+
{series.map((point, idx) => (
|
|
37
|
+
<TokenBar
|
|
38
|
+
key={point.day}
|
|
39
|
+
point={point}
|
|
40
|
+
idx={idx}
|
|
41
|
+
maxTotal={maxTotal}
|
|
42
|
+
barsReady={barsReady}
|
|
43
|
+
hovered={hovered === idx}
|
|
44
|
+
selected={selectedDay === point.day}
|
|
45
|
+
showDateLabel={idx % labelInterval === 0 || idx === series.length - 1}
|
|
46
|
+
onHover={setHovered}
|
|
47
|
+
onSelectDay={onSelectDay}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
33
52
|
<div
|
|
34
|
-
className={`absolute right-2 top-2 inline-flex h-6 items-center gap-1.5 rounded-full px-2.5 text-[10px] tracking-wide transition-opacity ${
|
|
53
|
+
className={`pointer-events-none absolute right-2 top-2 inline-flex h-6 items-center gap-1.5 rounded-full px-2.5 text-[10px] tracking-wide transition-opacity ${
|
|
35
54
|
active ? "bg-[var(--bg-secondary)]/85 text-[var(--text-primary)] opacity-100" : "text-[var(--text-secondary)] opacity-60"
|
|
36
55
|
}`}
|
|
37
56
|
>
|
|
38
|
-
{active
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<TokenBar
|
|
45
|
-
key={point.day}
|
|
46
|
-
point={point}
|
|
47
|
-
idx={idx}
|
|
48
|
-
maxTotal={maxTotal}
|
|
49
|
-
barsReady={barsReady}
|
|
50
|
-
hovered={hovered === idx}
|
|
51
|
-
selected={selectedDay === point.day}
|
|
52
|
-
onHover={setHovered}
|
|
53
|
-
onSelectDay={onSelectDay}
|
|
54
|
-
/>
|
|
55
|
-
))}
|
|
57
|
+
{active ? `${active.day} · ${formatInt(active.input_tokens_est + active.output_tokens_est)} tokens` : (
|
|
58
|
+
<>
|
|
59
|
+
<span className="hidden sm:inline">hover bars for details</span>
|
|
60
|
+
<span className="sm:hidden">tap bars; swipe for dates</span>
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
56
63
|
</div>
|
|
57
64
|
</div>
|
|
58
65
|
<TokenChartLegend active={active} />
|
|
@@ -67,11 +74,12 @@ interface TokenBarProps {
|
|
|
67
74
|
barsReady: boolean;
|
|
68
75
|
hovered: boolean;
|
|
69
76
|
selected: boolean;
|
|
77
|
+
showDateLabel: boolean;
|
|
70
78
|
onHover: (i: number | null) => void;
|
|
71
79
|
onSelectDay?: (day: string) => void;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
function TokenBar({ point, idx, maxTotal, barsReady, hovered, selected, onHover, onSelectDay }: TokenBarProps) {
|
|
82
|
+
function TokenBar({ point, idx, maxTotal, barsReady, hovered, selected, showDateLabel, onHover, onSelectDay }: TokenBarProps) {
|
|
75
83
|
const total = point.input_tokens_est + point.output_tokens_est;
|
|
76
84
|
const hasData = total > 0;
|
|
77
85
|
const totalHeight = hasData && maxTotal > 0 ? Math.max(4, Math.round((total / maxTotal) * 150)) : 0;
|
|
@@ -152,7 +160,12 @@ function TokenBar({ point, idx, maxTotal, barsReady, hovered, selected, onHover,
|
|
|
152
160
|
) : null}
|
|
153
161
|
</div>
|
|
154
162
|
<span className={`mt-1 text-[10px] truncate w-full text-center ${hasData ? "text-[var(--text-secondary)]" : "text-[var(--text-secondary)]/50"}`}>
|
|
155
|
-
{
|
|
163
|
+
{showDateLabel && (
|
|
164
|
+
<>
|
|
165
|
+
<span className="hidden sm:inline">{point.day.slice(5)}</span>
|
|
166
|
+
<span className="sm:hidden">{point.day.slice(8)}</span>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
156
169
|
</span>
|
|
157
170
|
</div>
|
|
158
171
|
);
|
|
@@ -17,7 +17,7 @@ export function SummaryStrip({ data, currencyInfo, windowTokenTotal, activeDays,
|
|
|
17
17
|
const cacheHitPct = (data.summary.cache_hit_rate * 100).toFixed(1);
|
|
18
18
|
const hasCacheStats = data.summary.cache_read_tokens > 0;
|
|
19
19
|
return (
|
|
20
|
-
<div className={`relative grid gap-2 ${hasCacheStats ? "md:grid-cols-5" : "md:grid-cols-4"}`}>
|
|
20
|
+
<div className={`relative grid grid-cols-2 gap-2 ${hasCacheStats ? "md:grid-cols-5" : "md:grid-cols-4"}`}>
|
|
21
21
|
<InsightChip
|
|
22
22
|
label="Window tokens"
|
|
23
23
|
value={formatInt(windowTokenTotal)}
|
|
@@ -243,12 +243,12 @@ export function NetworkSection() {
|
|
|
243
243
|
{mode !== "off" && (
|
|
244
244
|
<>
|
|
245
245
|
<label className="block text-xs text-fg-subtle">
|
|
246
|
-
No proxy (comma-separated, optional)
|
|
246
|
+
No proxy (comma-separated, optional; localhost is always direct)
|
|
247
247
|
<input
|
|
248
248
|
type="text"
|
|
249
249
|
value={noProxy}
|
|
250
250
|
onChange={(e) => setNoProxy(e.target.value)}
|
|
251
|
-
placeholder="
|
|
251
|
+
placeholder=".internal,corp.example"
|
|
252
252
|
className="mt-1 w-full px-2 py-1.5 text-sm rounded border border-border bg-surface-3 text-fg font-mono"
|
|
253
253
|
/>
|
|
254
254
|
</label>
|
|
@@ -20,6 +20,7 @@ import { BridgesPanel } from "@/components/bridges/BridgesPanel";
|
|
|
20
20
|
import { LogsPanel } from "@/components/logs/LogsPanel";
|
|
21
21
|
import { EnvVarsPanel } from "@/components/env/EnvVarsPanel";
|
|
22
22
|
import { SettingsPanel } from "@/components/settings/SettingsPanel";
|
|
23
|
+
import { UnifiedMemoryPanel } from "@/components/memory/UnifiedMemoryPanel";
|
|
23
24
|
import { HeaderActivity } from "@/components/ui/HeaderActivity";
|
|
24
25
|
import { NotificationStatus } from "@/components/ui/NotificationStatus";
|
|
25
26
|
import { CryptoFallbackBanner } from "@/components/ui/CryptoFallbackBanner";
|
|
@@ -35,7 +36,7 @@ import { getAppName } from "@/lib/env/app-config";
|
|
|
35
36
|
import { MenuPanel } from "./MenuPanel";
|
|
36
37
|
import { DashboardPanel } from "@/components/dashboard/DashboardPanel";
|
|
37
38
|
|
|
38
|
-
const ADVANCED_TABS = new Set(["bridges", "logs", "env"]);
|
|
39
|
+
const ADVANCED_TABS = new Set(["dashboard", "tasks", "tools", "memory", "bridges", "logs", "env"]);
|
|
39
40
|
|
|
40
41
|
export function AppShell() {
|
|
41
42
|
const { state, dispatch } = useAppContext();
|
|
@@ -304,6 +305,7 @@ export function AppShell() {
|
|
|
304
305
|
agents={agents}
|
|
305
306
|
agentsLoaded={agentsLoaded}
|
|
306
307
|
activeAgentId={state.activeAgentId}
|
|
308
|
+
preferredThreadId={state.activeThreadId}
|
|
307
309
|
onPickAgent={(id) => {
|
|
308
310
|
dispatch({ type: "SET_AGENT", agentId: id });
|
|
309
311
|
dispatch({ type: "SET_TAB", tab: "chat" });
|
|
@@ -468,7 +470,7 @@ export function AppShell() {
|
|
|
468
470
|
</button>
|
|
469
471
|
</header>
|
|
470
472
|
|
|
471
|
-
<div className="flex-1 relative overflow-hidden">
|
|
473
|
+
<div className="mx-auto flex-1 relative w-full max-w-7xl overflow-hidden">
|
|
472
474
|
{/*
|
|
473
475
|
Each tab is wrapped in <Activity> on its first visit, then kept
|
|
474
476
|
mounted so scroll position, expanded rows, and local form/filter
|
|
@@ -504,14 +506,23 @@ export function AppShell() {
|
|
|
504
506
|
<AgentsPanel />
|
|
505
507
|
</Activity>
|
|
506
508
|
)}
|
|
509
|
+
{isFullMode && mountedTabs.has("memory") && (
|
|
510
|
+
<Activity mode={state.activeTab === "memory" ? "visible" : "hidden"}>
|
|
511
|
+
<UnifiedMemoryPanel />
|
|
512
|
+
</Activity>
|
|
513
|
+
)}
|
|
507
514
|
{mountedTabs.has("models") && (
|
|
508
515
|
<Activity mode={state.activeTab === "models" ? "visible" : "hidden"}>
|
|
509
|
-
<
|
|
516
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
517
|
+
<ModelsPanel />
|
|
518
|
+
</div>
|
|
510
519
|
</Activity>
|
|
511
520
|
)}
|
|
512
521
|
{mountedTabs.has("credentials") && (
|
|
513
522
|
<Activity mode={state.activeTab === "credentials" ? "visible" : "hidden"}>
|
|
514
|
-
<
|
|
523
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
524
|
+
<CredentialsPanel />
|
|
525
|
+
</div>
|
|
515
526
|
</Activity>
|
|
516
527
|
)}
|
|
517
528
|
{mountedTabs.has("mcp") && (
|
|
@@ -541,12 +552,16 @@ export function AppShell() {
|
|
|
541
552
|
)}
|
|
542
553
|
{isFullMode && mountedTabs.has("logs") && (
|
|
543
554
|
<Activity mode={state.activeTab === "logs" ? "visible" : "hidden"}>
|
|
544
|
-
<
|
|
555
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
556
|
+
<LogsPanel />
|
|
557
|
+
</div>
|
|
545
558
|
</Activity>
|
|
546
559
|
)}
|
|
547
560
|
{isFullMode && mountedTabs.has("env") && (
|
|
548
561
|
<Activity mode={state.activeTab === "env" ? "visible" : "hidden"}>
|
|
549
|
-
<
|
|
562
|
+
<div className="mx-auto h-full w-full max-w-5xl min-w-0">
|
|
563
|
+
<EnvVarsPanel />
|
|
564
|
+
</div>
|
|
550
565
|
</Activity>
|
|
551
566
|
)}
|
|
552
567
|
{mountedTabs.has("settings") && (
|
|
@@ -557,7 +572,7 @@ export function AppShell() {
|
|
|
557
572
|
|
|
558
573
|
{showMenu && (
|
|
559
574
|
<div
|
|
560
|
-
className="fixed left-0 right-0 bottom-0 bg-black/
|
|
575
|
+
className="fixed left-0 right-0 bottom-0 bg-black/20 z-30"
|
|
561
576
|
style={{ top: "calc(3rem + var(--app-safe-top))" }}
|
|
562
577
|
onClick={() => setShowMenu(false)}
|
|
563
578
|
/>
|
|
@@ -38,22 +38,22 @@ const TAB_ICONS: Record<Tab, React.ReactNode> = {
|
|
|
38
38
|
|
|
39
39
|
const TAB_TITLES: Record<Tab, string> = {
|
|
40
40
|
chat: "Chat",
|
|
41
|
-
dashboard: "
|
|
41
|
+
dashboard: "Usage",
|
|
42
42
|
agents: "Agents",
|
|
43
|
-
memory: "
|
|
43
|
+
memory: "Knowledge",
|
|
44
44
|
documents: "Documents",
|
|
45
45
|
models: "Models",
|
|
46
46
|
credentials: "Credentials",
|
|
47
47
|
mcp: "MCP",
|
|
48
48
|
extensions: "Extensions",
|
|
49
|
-
tools: "
|
|
50
|
-
tasks: "
|
|
49
|
+
tools: "Capabilities",
|
|
50
|
+
tasks: "Automations",
|
|
51
51
|
bridges: "Bridges",
|
|
52
52
|
profile: "Profile",
|
|
53
53
|
harness: "Harness",
|
|
54
54
|
logs: "Logs",
|
|
55
55
|
env: "Defaults",
|
|
56
|
-
settings: "
|
|
56
|
+
settings: "Settings",
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// Tiny-screen labels (<=380px). Only abbreviate where the full name is
|
|
@@ -61,38 +61,28 @@ const TAB_TITLES: Record<Tab, string> = {
|
|
|
61
61
|
// "Me", "Setup") that overloads or contradicts the real meaning.
|
|
62
62
|
const TAB_SHORT: Record<Tab, string> = {
|
|
63
63
|
chat: "Chat",
|
|
64
|
-
dashboard: "
|
|
64
|
+
dashboard: "Usage",
|
|
65
65
|
agents: "Agents",
|
|
66
|
-
memory: "
|
|
66
|
+
memory: "Knowledge",
|
|
67
67
|
documents: "Docs",
|
|
68
68
|
models: "Models",
|
|
69
69
|
credentials: "Keys",
|
|
70
70
|
mcp: "MCP",
|
|
71
71
|
extensions: "Ext",
|
|
72
|
-
tools: "
|
|
73
|
-
tasks: "
|
|
72
|
+
tools: "Capabilities",
|
|
73
|
+
tasks: "Automations",
|
|
74
74
|
bridges: "Bridges",
|
|
75
75
|
profile: "Profile",
|
|
76
76
|
harness: "Tests",
|
|
77
77
|
logs: "Logs",
|
|
78
78
|
env: "Env",
|
|
79
|
-
settings: "
|
|
79
|
+
settings: "Settings",
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// menu grid.
|
|
87
|
-
//
|
|
88
|
-
// Capability-presence surfaces (documents, memory, MCP, extensions,
|
|
89
|
-
// bridges, built-in tool categories) all live under the "Tools" tab now —
|
|
90
|
-
// they were scattered across the top-level menu before but they're all
|
|
91
|
-
// answering the same question: "what can the agent see / do?". The legacy
|
|
92
|
-
// top-level Tab entries remain wired so deep-links (?tab=documents&item=…)
|
|
93
|
-
// still resolve, but they're hidden from the menu grid.
|
|
94
|
-
const COMMON_TABS: Tab[] = ["chat", "dashboard", "agents", "tools", "tasks", "profile", "settings"];
|
|
95
|
-
const ADVANCED_TABS: Tab[] = [];
|
|
82
|
+
// Keep everyday destinations visible; move the operational and capability
|
|
83
|
+
// panels behind Full mode's Advanced section.
|
|
84
|
+
const COMMON_TABS: Tab[] = ["chat", "agents", "settings", "profile"];
|
|
85
|
+
const ADVANCED_TABS: Tab[] = ["dashboard", "tasks", "tools", "memory"];
|
|
96
86
|
|
|
97
87
|
const ADVANCED_KEY = "jarela.menu.advanced";
|
|
98
88
|
|
|
@@ -42,7 +42,7 @@ export function ProfilePanel() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
|
-
<div className="
|
|
45
|
+
<div className="mx-auto flex h-full w-full max-w-5xl flex-col">
|
|
46
46
|
<PanelHeader icon={<User size={14} />} title="Profile">
|
|
47
47
|
{isEssential && (
|
|
48
48
|
<button
|
|
@@ -56,7 +56,7 @@ export function ProfilePanel() {
|
|
|
56
56
|
</button>
|
|
57
57
|
)}
|
|
58
58
|
</PanelHeader>
|
|
59
|
-
<div ref={containerRef} className="flex-1 overflow-y-auto no-scrollbar
|
|
59
|
+
<div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto no-scrollbar">
|
|
60
60
|
<ProfileEditor />
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Monitor, Moon, Palette, Sun } from "lucide-react";
|
|
2
|
+
import { Check, Monitor, Moon, Palette, Sun } from "lucide-react";
|
|
3
3
|
import { PanelHeader } from "@/components/ui/PanelHeader";
|
|
4
|
-
import { useTheme, type Theme } from "@/contexts/ThemeContext";
|
|
4
|
+
import { useTheme, type AccentColor, type SurfaceStyle, type Theme } from "@/contexts/ThemeContext";
|
|
5
5
|
import { useAppContext, type ExperienceMode } from "@/contexts/AppContext";
|
|
6
|
+
import { getAppAccentColor } from "@/lib/env/app-config";
|
|
6
7
|
|
|
7
8
|
// Visual + chrome settings that used to live in the MenuPanel footer
|
|
8
9
|
// (theme) and the top of the menu (Workspace mode). Hoisted here so
|
|
@@ -11,8 +12,9 @@ import { useAppContext, type ExperienceMode } from "@/contexts/AppContext";
|
|
|
11
12
|
|
|
12
13
|
export function AppearancePanel() {
|
|
13
14
|
const { state, dispatch } = useAppContext();
|
|
14
|
-
const { theme, setTheme } = useTheme();
|
|
15
|
+
const { theme, setTheme, accentColor, setAccentColor, surfaceStyle, setSurfaceStyle } = useTheme();
|
|
15
16
|
const isFullMode = state.experienceMode === "full";
|
|
17
|
+
const defaultAccent = getAppAccentColor() ?? "#2563eb";
|
|
16
18
|
|
|
17
19
|
const themeOptions: { value: Theme; label: string; icon: React.ReactNode; description: string }[] = [
|
|
18
20
|
{ value: "light", label: "Light", icon: <Sun size={14} />, description: "Bright UI; ignores system preference." },
|
|
@@ -25,6 +27,21 @@ export function AppearancePanel() {
|
|
|
25
27
|
{ value: "full", label: "Full", description: "Everything visible: engine-room tabs, power-user settings, all advanced sub-tabs." },
|
|
26
28
|
];
|
|
27
29
|
|
|
30
|
+
const hasBrandAccent = getAppAccentColor() !== null;
|
|
31
|
+
const accentOptions: { value: AccentColor; label: string; swatch: string }[] = [
|
|
32
|
+
{ value: "default", label: hasBrandAccent ? "Brand" : "Blue", swatch: defaultAccent },
|
|
33
|
+
...(hasBrandAccent ? [{ value: "blue" as const, label: "Blue", swatch: "#2563eb" }] : []),
|
|
34
|
+
{ value: "teal", label: "Teal", swatch: "#0f766e" },
|
|
35
|
+
{ value: "violet", label: "Violet", swatch: "#6d28d9" },
|
|
36
|
+
{ value: "rose", label: "Rose", swatch: "#be123c" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const surfaceOptions: { value: SurfaceStyle; label: string; description: string }[] = [
|
|
40
|
+
{ value: "ambient", label: "Atmospheric", description: "Keep the background glow and current glass surfaces." },
|
|
41
|
+
{ value: "calm", label: "Calm", description: "Keep navigation glass subtle and the background quiet." },
|
|
42
|
+
{ value: "liquid", label: "Liquid Glass", description: "Let background colors show through with brighter edge reflections." },
|
|
43
|
+
];
|
|
44
|
+
|
|
28
45
|
function setMode(mode: ExperienceMode) {
|
|
29
46
|
if (mode === state.experienceMode) return;
|
|
30
47
|
dispatch({ type: "SET_EXPERIENCE_MODE", mode });
|
|
@@ -35,6 +52,33 @@ export function AppearancePanel() {
|
|
|
35
52
|
<PanelHeader icon={<Palette size={14} />} title="Appearance" />
|
|
36
53
|
|
|
37
54
|
<div className="flex-1 overflow-y-auto no-scrollbar px-4 py-4 space-y-6">
|
|
55
|
+
<section>
|
|
56
|
+
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Accent color</h3>
|
|
57
|
+
<div className="flex flex-wrap gap-2">
|
|
58
|
+
{accentOptions.map((option) => {
|
|
59
|
+
const active = accentColor === option.value;
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
key={option.value}
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => setAccentColor(option.value)}
|
|
65
|
+
aria-label={`${option.label} accent color`}
|
|
66
|
+
aria-pressed={active}
|
|
67
|
+
title={option.label}
|
|
68
|
+
className={`control-tap inline-flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs transition-colors ${
|
|
69
|
+
active ? "border-accent bg-accent/10 text-fg" : "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
|
|
70
|
+
}`}
|
|
71
|
+
>
|
|
72
|
+
<span className="inline-flex h-4 w-4 items-center justify-center rounded-full" style={{ backgroundColor: option.swatch }}>
|
|
73
|
+
{active && <Check size={11} className="text-white" />}
|
|
74
|
+
</span>
|
|
75
|
+
{option.label}
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
38
82
|
<section>
|
|
39
83
|
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Theme</h3>
|
|
40
84
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
@@ -62,6 +106,29 @@ export function AppearancePanel() {
|
|
|
62
106
|
</div>
|
|
63
107
|
</section>
|
|
64
108
|
|
|
109
|
+
<section>
|
|
110
|
+
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Surface style</h3>
|
|
111
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
112
|
+
{surfaceOptions.map((option) => {
|
|
113
|
+
const active = surfaceStyle === option.value;
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
key={option.value}
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => setSurfaceStyle(option.value)}
|
|
119
|
+
aria-pressed={active}
|
|
120
|
+
className={`text-left rounded-lg border px-3 py-2.5 transition-colors ${
|
|
121
|
+
active ? "border-accent bg-accent/10 text-fg" : "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
<div className="text-sm font-medium text-fg mb-1">{option.label}</div>
|
|
125
|
+
<p className="text-[11px] text-fg-faint leading-snug">{option.description}</p>
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
65
132
|
<section>
|
|
66
133
|
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Workspace mode</h3>
|
|
67
134
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|