@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
|
@@ -130,10 +130,13 @@ function fuzzyUniqueRange(raw: string, oldString: string): MatchRange[] {
|
|
|
130
130
|
if (oldLines.some((line) => line.length === 0)) return [];
|
|
131
131
|
const rawLines = raw.split(/\r\n|\r|\n/);
|
|
132
132
|
const rawLineStarts: number[] = [];
|
|
133
|
+
const eol = /\r\n|\r|\n/g;
|
|
133
134
|
let offset = 0;
|
|
134
135
|
for (const line of rawLines) {
|
|
135
136
|
rawLineStarts.push(offset);
|
|
136
|
-
offset
|
|
137
|
+
eol.lastIndex = offset + line.length;
|
|
138
|
+
const brk = eol.exec(raw);
|
|
139
|
+
offset = offset + line.length + (brk ? brk[0].length : 0);
|
|
137
140
|
}
|
|
138
141
|
const scores: Array<{ score: number; start: number; end: number }> = [];
|
|
139
142
|
for (let i = 0; i + oldLines.length <= rawLines.length; i++) {
|
|
@@ -188,7 +191,9 @@ function findAllExact(haystack: string, needle: string): number[] {
|
|
|
188
191
|
function findWithTransforms(raw: string, oldString: string, fns: Array<(s: string) => Transform>): MatchRange[] {
|
|
189
192
|
const bufT = composeTransforms(raw, fns);
|
|
190
193
|
const oldT = composeTransforms(oldString, fns);
|
|
191
|
-
|
|
194
|
+
// A needle that normalizes to bare whitespace would match an arbitrary
|
|
195
|
+
// line break, so it can never identify an edit target.
|
|
196
|
+
if (oldT.text.trim().length === 0) return [];
|
|
192
197
|
const starts = findAllExact(bufT.text, oldT.text);
|
|
193
198
|
return starts.map((s) => ({
|
|
194
199
|
start: bufT.toOriginal[s],
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
registerDefaultPackages,
|
|
15
15
|
_resetDefaultPackages,
|
|
16
16
|
} = await import("./default-packages");
|
|
17
|
+
const { registeredTools, registeredCategory, registeredCapability } = await import("../runtime/registry");
|
|
17
18
|
|
|
18
19
|
const { setPackageDisabled, listDisabledPackages } = await import(
|
|
19
20
|
"@/lib/stores/disabled-packages"
|
|
@@ -52,6 +53,26 @@ describe("default LangChain packages", () => {
|
|
|
52
53
|
expect(listDefaultPackages().every((p) => p.installed === false)).toBe(true);
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
it("registers workspace packages with their declared categories and capabilities", () => {
|
|
57
|
+
const previousPackagesDir = process.env.JARELA_PACKAGES_DIR;
|
|
58
|
+
process.env.JARELA_PACKAGES_DIR = "";
|
|
59
|
+
try {
|
|
60
|
+
registerDefaultPackages();
|
|
61
|
+
|
|
62
|
+
expect(listDefaultPackages().every((p) => p.installed)).toBe(true);
|
|
63
|
+
const tools = registeredTools();
|
|
64
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
65
|
+
for (const tool of tools) {
|
|
66
|
+
expect(registeredCategory(tool.name)).toBeDefined();
|
|
67
|
+
expect(["read", "write", "execute"]).toContain(registeredCapability(tool.name));
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
_resetDefaultPackages();
|
|
71
|
+
if (previousPackagesDir === undefined) delete process.env.JARELA_PACKAGES_DIR;
|
|
72
|
+
else process.env.JARELA_PACKAGES_DIR = previousPackagesDir;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
55
76
|
it("findDefaultPackage returns the descriptor for a known id", () => {
|
|
56
77
|
const d = findDefaultPackage("github");
|
|
57
78
|
expect(d).not.toBeNull();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { isOptionalPackageInstalled, loadOptionalPackage } from "./optional-package";
|
|
6
|
+
|
|
7
|
+
describe("optional package resolution", () => {
|
|
8
|
+
let managedDir: string;
|
|
9
|
+
let originalPackagesDir: string | undefined;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalPackagesDir = process.env.JARELA_PACKAGES_DIR;
|
|
13
|
+
managedDir = mkdtempSync(join(tmpdir(), "jarela-optional-packages-"));
|
|
14
|
+
process.env.JARELA_PACKAGES_DIR = managedDir;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (originalPackagesDir === undefined) delete process.env.JARELA_PACKAGES_DIR;
|
|
19
|
+
else process.env.JARELA_PACKAGES_DIR = originalPackagesDir;
|
|
20
|
+
rmSync(managedDir, { recursive: true, force: true });
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("prefers a package installed in the managed packages directory", () => {
|
|
25
|
+
const packageDir = join(managedDir, "node_modules", "fake-optional-package");
|
|
26
|
+
mkdirSync(packageDir, { recursive: true });
|
|
27
|
+
writeFileSync(join(packageDir, "package.json"), JSON.stringify({
|
|
28
|
+
name: "fake-optional-package",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
main: "index.cjs",
|
|
31
|
+
}));
|
|
32
|
+
writeFileSync(join(packageDir, "index.cjs"), 'module.exports = { source: "managed" };');
|
|
33
|
+
|
|
34
|
+
expect(loadOptionalPackage("fake-optional-package")).toEqual({ source: "managed" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
"@circuitwall/atlassian-langchain",
|
|
39
|
+
"@circuitwall/github-langchain",
|
|
40
|
+
"@circuitwall/icloud-langchain",
|
|
41
|
+
"@circuitwall/jira-align-langchain",
|
|
42
|
+
"@circuitwall/ms-todo-langchain",
|
|
43
|
+
])("falls back to the app workspace for %s", (packageName) => {
|
|
44
|
+
delete process.env.JARELA_PACKAGES_DIR;
|
|
45
|
+
expect(loadOptionalPackage(packageName)).not.toBeNull();
|
|
46
|
+
expect(isOptionalPackageInstalled(packageName)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("exposes exports from the workspace fallback", () => {
|
|
50
|
+
delete process.env.JARELA_PACKAGES_DIR;
|
|
51
|
+
const loaded = loadOptionalPackage<{ githubFetch?: unknown }>("@circuitwall/github-langchain");
|
|
52
|
+
|
|
53
|
+
expect(loaded).not.toBeNull();
|
|
54
|
+
expect(typeof loaded?.githubFetch).toBe("function");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("treats an absent optional package as a normal miss", () => {
|
|
58
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
59
|
+
|
|
60
|
+
expect(loadOptionalPackage("@circuitwall/not-installed")).toBeNull();
|
|
61
|
+
expect(error).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not treat unrelated app dependencies as installed optional packages", () => {
|
|
65
|
+
expect(loadOptionalPackage("@langchain/core")).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("honors an explicit managed directory even when a workspace package exists", () => {
|
|
69
|
+
expect(loadOptionalPackage("@circuitwall/github-langchain")).toBeNull();
|
|
70
|
+
expect(isOptionalPackageInstalled("@circuitwall/github-langchain")).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -22,12 +22,40 @@ function managedPackagePath(packageName: string): string {
|
|
|
22
22
|
return join(getPackagesDir(), "node_modules", packageName);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface ResolvedOptionalPackage {
|
|
26
|
+
req: OptionalRequire;
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveOptionalPackage(packageName: string): ResolvedOptionalPackage | null {
|
|
31
|
+
const managedReq = createRuntimeRequire(join(getPackagesDir(), "_anchor"));
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
req: managedReq,
|
|
35
|
+
path: managedReq.resolve(managedPackagePath(packageName)),
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
// Workspace builds expose bundled @circuitwall packages from the app root.
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.JARELA_PACKAGES_DIR?.trim()) return null;
|
|
42
|
+
if (!packageName.startsWith("@circuitwall/")) return null;
|
|
43
|
+
|
|
44
|
+
const appReq = createRuntimeRequire(join(process.cwd(), "package.json"));
|
|
45
|
+
try {
|
|
46
|
+
return { req: appReq, path: appReq.resolve(packageName) };
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
export function loadOptionalPackage<T extends AnyModule>(packageName: string): T | null {
|
|
26
53
|
const override = testOverrides.get(packageName);
|
|
27
54
|
if (override) return override as T;
|
|
55
|
+
const resolved = resolveOptionalPackage(packageName);
|
|
56
|
+
if (!resolved) return null;
|
|
28
57
|
try {
|
|
29
|
-
|
|
30
|
-
return req(managedPackagePath(packageName)) as T;
|
|
58
|
+
return resolved.req(resolved.path) as T;
|
|
31
59
|
} catch (err) {
|
|
32
60
|
// Keep the optional boundary non-fatal, but leave the actual load failure
|
|
33
61
|
// visible so missing peer dependencies are diagnosable from server logs.
|
|
@@ -68,9 +96,10 @@ export function optionalFunction<T extends (...args: any[]) => any>(
|
|
|
68
96
|
}
|
|
69
97
|
|
|
70
98
|
export function isOptionalPackageInstalled(packageName: string): boolean {
|
|
99
|
+
const resolved = resolveOptionalPackage(packageName);
|
|
100
|
+
if (!resolved) return false;
|
|
71
101
|
try {
|
|
72
|
-
|
|
73
|
-
req(managedPackagePath(packageName));
|
|
102
|
+
resolved.req(resolved.path);
|
|
74
103
|
return true;
|
|
75
104
|
} catch (err) {
|
|
76
105
|
console.error(`[optional-package] failed to load ${packageName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-result-refs-"));
|
|
7
|
+
process.env.JARELA_DB_DIR = tmpRoot;
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
getToolResultMaxBytes,
|
|
11
|
+
parseToolResultReferenceEnvelope,
|
|
12
|
+
postProcessToolResult,
|
|
13
|
+
readToolResultRef,
|
|
14
|
+
serializeToolResult,
|
|
15
|
+
} = await import("./result-refs");
|
|
16
|
+
|
|
17
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
18
|
+
afterAll(() => rmSync(tmpRoot, { recursive: true, force: true }));
|
|
19
|
+
|
|
20
|
+
async function spill(result: unknown, maxBytes = 64) {
|
|
21
|
+
vi.stubEnv("JARELA_TOOL_RESULT_MAX_BYTES", String(maxBytes));
|
|
22
|
+
const envelope = parseToolResultReferenceEnvelope(await postProcessToolResult("my tool/x", result));
|
|
23
|
+
if (!envelope) throw new Error("expected a reference envelope");
|
|
24
|
+
return envelope;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readAll(name: string, limit: number) {
|
|
28
|
+
const pages: string[] = [];
|
|
29
|
+
let offset: number | null = 0;
|
|
30
|
+
for (let guard = 0; offset !== null && guard < 10_000; guard++) {
|
|
31
|
+
const page = await readToolResultRef({ name, offset, limit });
|
|
32
|
+
expect(page.ok).toBe(true);
|
|
33
|
+
pages.push(page.result as string);
|
|
34
|
+
offset = page.next_offset as number | null;
|
|
35
|
+
}
|
|
36
|
+
return pages;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("max inline bytes", () => {
|
|
40
|
+
it("ignores invalid overrides", () => {
|
|
41
|
+
for (const bad of ["0", "-5", "abc", "Infinity"]) {
|
|
42
|
+
vi.stubEnv("JARELA_TOOL_RESULT_MAX_BYTES", bad);
|
|
43
|
+
expect(getToolResultMaxBytes()).toBe(16 * 1024);
|
|
44
|
+
}
|
|
45
|
+
vi.stubEnv("JARELA_TOOL_RESULT_MAX_BYTES", "100.9");
|
|
46
|
+
expect(getToolResultMaxBytes()).toBe(100);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("postProcessToolResult", () => {
|
|
51
|
+
it("passes small results through unchanged", async () => {
|
|
52
|
+
vi.stubEnv("JARELA_TOOL_RESULT_MAX_BYTES", "1000");
|
|
53
|
+
expect(await postProcessToolResult("t", { a: 1 })).toBe('{"a":1}');
|
|
54
|
+
expect(serializeToolResult(undefined)).toBe("undefined");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("measures the limit in bytes, not characters", async () => {
|
|
58
|
+
vi.stubEnv("JARELA_TOOL_RESULT_MAX_BYTES", "10");
|
|
59
|
+
// 4 characters, 16 bytes.
|
|
60
|
+
const out = await postProcessToolResult("t", "😀😀😀😀");
|
|
61
|
+
expect(parseToolResultReferenceEnvelope(out)?.bytes).toBe(16);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("sanitizes the tool name and records the total array length", async () => {
|
|
65
|
+
const envelope = await spill(Array.from({ length: 50 }, (_, i) => ({ i })));
|
|
66
|
+
expect(envelope.result_ref.mimeType).toBe("application/json");
|
|
67
|
+
expect(envelope.total_count).toBe(50);
|
|
68
|
+
expect((envelope.preview as unknown[]).length).toBe(20);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps array previews within the preview byte budget when rows are huge", async () => {
|
|
72
|
+
const rows = Array.from({ length: 3 }, (_, i) => ({ i, body: "x".repeat(50_000) }));
|
|
73
|
+
const envelope = await spill(rows);
|
|
74
|
+
expect(envelope.preview_bytes).toBeLessThanOrEqual(4 * 1024);
|
|
75
|
+
expect(typeof envelope.preview).toBe("string");
|
|
76
|
+
expect(Buffer.byteLength(envelope.preview as string)).toBe(envelope.preview_bytes);
|
|
77
|
+
expect(envelope.total_count).toBe(3);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("never ends a text preview on half of a surrogate pair", async () => {
|
|
81
|
+
const envelope = await spill("a" + "😀".repeat(3000));
|
|
82
|
+
const preview = envelope.preview as string;
|
|
83
|
+
expect(Buffer.byteLength(preview)).toBeLessThanOrEqual(4 * 1024);
|
|
84
|
+
expect(preview.isWellFormed()).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("readToolResultRef", () => {
|
|
89
|
+
it("pages multi-byte text without corrupting characters at page edges", async () => {
|
|
90
|
+
const text = "héllo wörld — 日本語のテキスト 😀🎉 ".repeat(40);
|
|
91
|
+
const envelope = await spill(text);
|
|
92
|
+
for (const limit of [1, 2, 3, 5, 7, 64]) {
|
|
93
|
+
const pages = await readAll(envelope.result_ref.name, limit);
|
|
94
|
+
expect(pages.join("")).toBe(text);
|
|
95
|
+
expect(pages.every((p) => !p.includes("\uFFFD"))).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("realigns a caller offset that lands inside a character", async () => {
|
|
100
|
+
const envelope = await spill("€".repeat(100)); // 3 bytes each
|
|
101
|
+
const page = await readToolResultRef({ name: envelope.result_ref.name, offset: 4, limit: 6 });
|
|
102
|
+
expect(page).toMatchObject({ offset: 3, result: "€€", next_offset: 9 });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns an empty final page for offsets past the end", async () => {
|
|
106
|
+
const envelope = await spill("z".repeat(200));
|
|
107
|
+
const page = await readToolResultRef({ name: envelope.result_ref.name, offset: 10_000 });
|
|
108
|
+
expect(page).toMatchObject({ ok: true, result: "", next_offset: null, done: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("clamps negative and fractional offsets and limits", async () => {
|
|
112
|
+
const envelope = await spill("abcdefghij".repeat(10));
|
|
113
|
+
const page = await readToolResultRef({ name: envelope.result_ref.name, offset: -3.7, limit: 2.9 });
|
|
114
|
+
expect(page).toMatchObject({ offset: 0, limit: 2, result: "ab", next_offset: 2 });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects path traversal and unknown refs without throwing", async () => {
|
|
118
|
+
expect(await readToolResultRef({ name: "../secrets.json" })).toMatchObject({ ok: false, error: "unsafe result_ref name" });
|
|
119
|
+
expect(await readToolResultRef({ name: "deadbeef.json" })).toMatchObject({ ok: false, status: "unknown" });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("parseToolResultReferenceEnvelope", () => {
|
|
124
|
+
it("rejects look-alike payloads", () => {
|
|
125
|
+
expect(parseToolResultReferenceEnvelope("not json")).toBeNull();
|
|
126
|
+
expect(parseToolResultReferenceEnvelope({ ok: true, truncated: true, bytes: 1 })).toBeNull();
|
|
127
|
+
expect(parseToolResultReferenceEnvelope({ ok: true, truncated: "yes", bytes: 1, result_ref: { name: "a" } })).toBeNull();
|
|
128
|
+
expect(parseToolResultReferenceEnvelope('{"ok":true,"truncated":true,"bytes":"1","result_ref":{"name":"a"}}')).toBeNull();
|
|
129
|
+
expect(parseToolResultReferenceEnvelope(null)).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -93,23 +93,27 @@ export async function readToolResultRef(args: {
|
|
|
93
93
|
}
|
|
94
94
|
const offset = Math.max(0, Math.floor(args.offset ?? 0));
|
|
95
95
|
const limit = Math.min(MAX_READ_BYTES, Math.max(1, Math.floor(args.limit ?? DEFAULT_PREVIEW_BYTES)));
|
|
96
|
-
let buf: Buffer;
|
|
97
96
|
try {
|
|
98
97
|
const file = await fsp.open(abs, "r");
|
|
99
98
|
try {
|
|
100
99
|
const stat = await file.stat();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
100
|
+
// Read a few bytes either side so the window can be moved onto UTF-8
|
|
101
|
+
// character boundaries; paging must never split a multi-byte character.
|
|
102
|
+
const windowStart = Math.min(Math.max(0, offset - 3), stat.size);
|
|
103
|
+
const windowLength = Math.max(0, Math.min(stat.size - windowStart, offset - windowStart + limit + 3));
|
|
104
|
+
const buf = Buffer.alloc(windowLength);
|
|
105
|
+
if (windowLength > 0) await file.read(buf, 0, windowLength, windowStart);
|
|
106
|
+
const { start, end } = utf8Window(buf, Math.min(offset, stat.size) - windowStart, limit);
|
|
107
|
+
const actualOffset = windowStart + start;
|
|
108
|
+
const nextOffset = windowStart + end;
|
|
105
109
|
return {
|
|
106
110
|
ok: true,
|
|
107
111
|
status: "done",
|
|
108
112
|
name: args.name,
|
|
109
|
-
offset,
|
|
113
|
+
offset: actualOffset,
|
|
110
114
|
limit,
|
|
111
115
|
bytes: stat.size,
|
|
112
|
-
result: buf.toString("utf8"),
|
|
116
|
+
result: buf.subarray(start, end).toString("utf8"),
|
|
113
117
|
next_offset: nextOffset < stat.size ? nextOffset : null,
|
|
114
118
|
done: nextOffset >= stat.size,
|
|
115
119
|
};
|
|
@@ -126,6 +130,25 @@ export async function readToolResultRef(args: {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
const isUtf8Continuation = (byte: number | undefined) => byte !== undefined && (byte & 0xc0) === 0x80;
|
|
134
|
+
|
|
135
|
+
// Moves `[start, start + limit)` inside `buf` onto character boundaries: the
|
|
136
|
+
// start backs up to its lead byte, and the end backs up so it does not cut a
|
|
137
|
+
// character — unless that character alone exceeds `limit`, in which case the
|
|
138
|
+
// whole character is returned so paging always makes progress.
|
|
139
|
+
function utf8Window(buf: Buffer, start: number, limit: number): { start: number; end: number } {
|
|
140
|
+
let s = Math.max(0, Math.min(start, buf.length));
|
|
141
|
+
while (s > 0 && isUtf8Continuation(buf[s])) s--;
|
|
142
|
+
let e = Math.min(buf.length, s + limit);
|
|
143
|
+
if (e < buf.length && isUtf8Continuation(buf[e])) {
|
|
144
|
+
let lead = e;
|
|
145
|
+
while (lead > s && isUtf8Continuation(buf[lead])) lead--;
|
|
146
|
+
if (lead > s) e = lead;
|
|
147
|
+
else while (e < buf.length && isUtf8Continuation(buf[e])) e++;
|
|
148
|
+
}
|
|
149
|
+
return { start: s, end: e };
|
|
150
|
+
}
|
|
151
|
+
|
|
129
152
|
export function serializeToolResult(result: unknown): string {
|
|
130
153
|
if (typeof result === "string") return result;
|
|
131
154
|
return JSON.stringify(result) ?? String(result);
|
|
@@ -145,8 +168,15 @@ function buildPreview(value: string, mimeType: string): { value: unknown; bytes:
|
|
|
145
168
|
try {
|
|
146
169
|
const parsed = JSON.parse(value) as unknown;
|
|
147
170
|
if (Array.isArray(parsed)) {
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
// Cap by bytes as well as items: a few huge rows must not smuggle the
|
|
172
|
+
// whole result back inline.
|
|
173
|
+
for (let n = Math.min(parsed.length, JSON_ARRAY_PREVIEW_ITEMS); n > 0; n--) {
|
|
174
|
+
const preview = parsed.slice(0, n);
|
|
175
|
+
const bytes = Buffer.byteLength(JSON.stringify(preview));
|
|
176
|
+
if (bytes <= DEFAULT_PREVIEW_BYTES) return { value: preview, bytes, total_count: parsed.length };
|
|
177
|
+
}
|
|
178
|
+
const text = takeUtf8Prefix(value, DEFAULT_PREVIEW_BYTES);
|
|
179
|
+
return { value: text, bytes: Buffer.byteLength(text), total_count: parsed.length };
|
|
150
180
|
}
|
|
151
181
|
} catch {
|
|
152
182
|
// Fall through to byte preview.
|
|
@@ -165,6 +195,8 @@ function takeUtf8Prefix(value: string, maxBytes: number): string {
|
|
|
165
195
|
if (Buffer.byteLength(value.slice(0, mid)) <= maxBytes) lo = mid;
|
|
166
196
|
else hi = mid - 1;
|
|
167
197
|
}
|
|
198
|
+
const code = value.charCodeAt(lo - 1);
|
|
199
|
+
if (lo > 0 && code >= 0xd800 && code <= 0xdbff) lo--;
|
|
168
200
|
return value.slice(0, lo);
|
|
169
201
|
}
|
|
170
202
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseHref } from "./navigate";
|
|
3
|
+
|
|
4
|
+
describe("parseHref legacy Knowledge routes", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
"/?tab=documents",
|
|
7
|
+
"/?tab=memory",
|
|
8
|
+
"/?tab=tools&item=documents",
|
|
9
|
+
"/?tab=tools&item=memory",
|
|
10
|
+
])("routes %s to Knowledge", (href) => {
|
|
11
|
+
expect(parseHref(href)).toMatchObject({ tab: "memory", item: undefined, external: false });
|
|
12
|
+
});
|
|
13
|
+
});
|
package/lib/ui/navigate.ts
CHANGED
|
@@ -40,8 +40,16 @@ export function parseHref(input: string | null | undefined): ParsedHref {
|
|
|
40
40
|
|
|
41
41
|
const params = new URLSearchParams(queryStr);
|
|
42
42
|
const tabRaw = params.get("tab");
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
let tab = TABS.includes(tabRaw as Tab) ? (tabRaw as Tab) : undefined;
|
|
44
|
+
let item = params.get("item") ?? undefined;
|
|
45
|
+
if (
|
|
46
|
+
tab === "documents" ||
|
|
47
|
+
tab === "memory" ||
|
|
48
|
+
(tab === "tools" && (item === "memory" || item === "documents"))
|
|
49
|
+
) {
|
|
50
|
+
tab = "memory";
|
|
51
|
+
item = undefined;
|
|
52
|
+
}
|
|
45
53
|
const thread = params.get("thread") ?? undefined;
|
|
46
54
|
const agent = params.get("agent") ?? undefined;
|
|
47
55
|
return { tab, item, thread, agent, hash, external: false };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { citeAs, httpsUrlOrNull } from "./source-link";
|
|
3
|
+
|
|
4
|
+
describe("httpsUrlOrNull", () => {
|
|
5
|
+
it("accepts absolute https URLs", () => {
|
|
6
|
+
expect(httpsUrlOrNull("https://outlook.office365.com/owa/?ItemID=AAk%3D&exvsurl=1")).toBe(
|
|
7
|
+
"https://outlook.office365.com/owa/?ItemID=AAk%3D&exvsurl=1",
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("rejects missing, relative, and non-https URLs", () => {
|
|
12
|
+
for (const url of [null, undefined, "", "/mail/123", "http://example.com", "javascript:alert(1)", "message://%3cid%3e", "not a url"]) {
|
|
13
|
+
expect(httpsUrlOrNull(url)).toBeNull();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("citeAs", () => {
|
|
19
|
+
it("builds a markdown link from a label and URL", () => {
|
|
20
|
+
expect(citeAs("Quarterly report", "https://www.google.com/calendar/event?eid=abc")).toBe(
|
|
21
|
+
"[Quarterly report](https://www.google.com/calendar/event?eid=abc)",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns null without a usable URL", () => {
|
|
26
|
+
expect(citeAs("Subject", null)).toBeNull();
|
|
27
|
+
expect(citeAs("Subject", "http://insecure.example")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("escapes brackets and flattens whitespace in the label", () => {
|
|
31
|
+
expect(citeAs("Re: [EXT]\n budget\\plan", "https://x.test/a")).toBe("[Re: \\[EXT\\] budget\\\\plan](https://x.test/a)");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("falls back to a default label for an empty subject", () => {
|
|
35
|
+
expect(citeAs(" ", "https://x.test/a", "Outlook message")).toBe("[Outlook message](https://x.test/a)");
|
|
36
|
+
expect(citeAs(null, "https://x.test/a")).toBe("[source](https://x.test/a)");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("caps long labels", () => {
|
|
40
|
+
const link = citeAs("x".repeat(200), "https://x.test/a")!;
|
|
41
|
+
const label = link.slice(1, link.indexOf("]("));
|
|
42
|
+
expect(label.length).toBe(80);
|
|
43
|
+
expect(label.endsWith("…")).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("encodes characters that would end the markdown destination", () => {
|
|
47
|
+
const link = citeAs("Event", "https://x.test/path(1)?q=a b")!;
|
|
48
|
+
expect(link).toBe("[Event](https://x.test/path%281%29?q=a%20b)");
|
|
49
|
+
expect(/^\[[^\]]*\]\([^()\s]+\)$/.test(link)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-link fields for tool results, so an agent can link a reply back to
|
|
3
|
+
* the email, calendar event, or chat it came from without building URLs.
|
|
4
|
+
*
|
|
5
|
+
* `cite_as` is a ready-made markdown link the system prompt tells agents to
|
|
6
|
+
* paste verbatim. Building it in code keeps the label and URL well-formed:
|
|
7
|
+
* a stray `]` in a subject or `)` in a URL would otherwise break the link.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_LABEL_CHARS = 80;
|
|
11
|
+
|
|
12
|
+
/** Returns `url` when it is an absolute https URL, otherwise null. */
|
|
13
|
+
export function httpsUrlOrNull(url: string | null | undefined): string | null {
|
|
14
|
+
if (!url) return null;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(url);
|
|
17
|
+
return parsed.protocol === "https:" ? parsed.href : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function markdownLabel(label: string | null | undefined, fallback: string): string {
|
|
24
|
+
const flat = (label ?? "").replace(/\s+/g, " ").trim();
|
|
25
|
+
const capped = flat.length > MAX_LABEL_CHARS ? `${flat.slice(0, MAX_LABEL_CHARS - 1).trimEnd()}…` : flat;
|
|
26
|
+
return (capped || fallback).replace(/[\\[\]]/g, (c) => `\\${c}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function markdownDestination(url: string): string {
|
|
30
|
+
return url.replace(/[()\s<>]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pre-built markdown link for `cite_as`, or null when there is no usable URL.
|
|
35
|
+
* Only https URLs qualify: the chat renderer drops other schemes.
|
|
36
|
+
*/
|
|
37
|
+
export function citeAs(
|
|
38
|
+
label: string | null | undefined,
|
|
39
|
+
url: string | null | undefined,
|
|
40
|
+
fallbackLabel = "source",
|
|
41
|
+
): string | null {
|
|
42
|
+
const safe = httpsUrlOrNull(url);
|
|
43
|
+
if (!safe) return null;
|
|
44
|
+
return `[${markdownLabel(label, fallbackLabel)}](${markdownDestination(safe)})`;
|
|
45
|
+
}
|
package/package.json
CHANGED