@oar-sh/oar 0.9.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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Known-CWD option list shared by the Change CWD modal (cwd-picker.js) and the
|
|
2
|
+
// New Chat modal (journal-view.js). Pure module: callers pass in the store and
|
|
3
|
+
// repo-browser values so the list stays testable without a DOM.
|
|
4
|
+
|
|
5
|
+
// Mirrors normalizeDriveLetterOnlyPath in server/services/workspace-root-path-policy.mjs.
|
|
6
|
+
// It cannot be imported: only server/public is served to the browser.
|
|
7
|
+
export function normalizeKnownCwdPath(value) {
|
|
8
|
+
const stripped = String(value || '').trim().replace(/[\\/]+$/, '');
|
|
9
|
+
// Always restore the trailing backslash for Windows drive roots ("D:" → "D:\").
|
|
10
|
+
// Without it, sending "D:" to the server causes path.resolve("D:") to return the
|
|
11
|
+
// server's remembered CWD for drive D, not the drive root.
|
|
12
|
+
if (/^[A-Za-z]:$/.test(stripped)) return `${stripped}\\`;
|
|
13
|
+
return stripped;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildKnownCwdOptions({
|
|
17
|
+
currentSessionCwd = '',
|
|
18
|
+
workspaceRootPath = '',
|
|
19
|
+
browserCwd = '',
|
|
20
|
+
recentRoots = [],
|
|
21
|
+
} = {}) {
|
|
22
|
+
const options = [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const add = (label, value, note = '') => {
|
|
25
|
+
const pathValue = normalizeKnownCwdPath(value);
|
|
26
|
+
if (!pathValue) return;
|
|
27
|
+
const key = pathValue.toLowerCase();
|
|
28
|
+
if (seen.has(key)) return;
|
|
29
|
+
seen.add(key);
|
|
30
|
+
options.push({ label, path: pathValue, note });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
add('Current session CWD', currentSessionCwd, 'Selected session');
|
|
34
|
+
add('Relay workspace', workspaceRootPath, 'Relay host cwd');
|
|
35
|
+
add('Current browser folder', browserCwd, 'From file explorer');
|
|
36
|
+
const history = Array.isArray(recentRoots) ? recentRoots : [];
|
|
37
|
+
history.forEach((pathValue, index) => {
|
|
38
|
+
add(`Recent CWD ${index + 1}`, pathValue, 'Relay history');
|
|
39
|
+
});
|
|
40
|
+
return options;
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Joining the workspace root with a tree-relative path for launch-CWD use
|
|
2
|
+
// (repo browser → known-CWD lists / CWD pickers). Pure module so the separator
|
|
3
|
+
// inference is unit-testable under plain node.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A root is Windows-style when it starts with a drive letter or already
|
|
7
|
+
* contains a backslash. Only then is the join done with `\` — a POSIX root
|
|
8
|
+
* (`/home/...`) keeps `/`, which is what the relay host actually resolves.
|
|
9
|
+
*/
|
|
10
|
+
export function isWindowsStyleRoot(basePath) {
|
|
11
|
+
const root = String(basePath || '').trim();
|
|
12
|
+
return /^[A-Za-z]:/.test(root) || root.includes('\\');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function joinLaunchCwdPath(basePath, relativePath) {
|
|
16
|
+
const root = String(basePath || '').trim().replace(/[\\/]+$/, '');
|
|
17
|
+
const relRaw = String(relativePath || '').trim().replace(/^[\\/]+/, '');
|
|
18
|
+
const windowsStyle = isWindowsStyleRoot(root);
|
|
19
|
+
const rel = windowsStyle ? relRaw.replace(/\//g, '\\') : relRaw.replace(/\\/g, '/');
|
|
20
|
+
if (!root) return rel;
|
|
21
|
+
if (!rel) return root;
|
|
22
|
+
return `${root}${windowsStyle ? '\\' : '/'}${rel}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { stripRelayPromptContext } from './relay-prompt-sanitizer.mjs';
|
|
2
|
+
|
|
3
|
+
function normalizeText(value) {
|
|
4
|
+
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizeId(value) {
|
|
8
|
+
const text = String(value || '').trim();
|
|
9
|
+
return text || '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeRole(value) {
|
|
13
|
+
return String(value || '').trim().toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeTimestampMs(value) {
|
|
17
|
+
const parsed = Date.parse(String(value || '').trim());
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildLiveMessageFingerprint(message = {}) {
|
|
22
|
+
const sanitizedText = stripRelayPromptContext(message?.text, message?.mode);
|
|
23
|
+
return {
|
|
24
|
+
id: normalizeId(message?.id),
|
|
25
|
+
role: normalizeRole(message?.role),
|
|
26
|
+
text: normalizeText(sanitizedText),
|
|
27
|
+
timestampMs: normalizeTimestampMs(message?.timestamp),
|
|
28
|
+
sourceMessageId: normalizeId(message?.sourceMessageId),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isLikelyLiveDuplicateMessage({
|
|
33
|
+
incomingMessageId = '',
|
|
34
|
+
incomingMessage = null,
|
|
35
|
+
existingMessages = [],
|
|
36
|
+
timestampWindowMs = 15_000,
|
|
37
|
+
hasPendingTextMatch = false,
|
|
38
|
+
} = {}) {
|
|
39
|
+
const incoming = buildLiveMessageFingerprint({
|
|
40
|
+
...(incomingMessage && typeof incomingMessage === 'object' ? incomingMessage : {}),
|
|
41
|
+
id: incomingMessageId || incomingMessage?.id || '',
|
|
42
|
+
});
|
|
43
|
+
if (!incoming.role || !incoming.text) return false;
|
|
44
|
+
if (incoming.role === 'user' && hasPendingTextMatch) return true;
|
|
45
|
+
const timeWindowMs = Math.max(1_000, Number(timestampWindowMs) || 15_000);
|
|
46
|
+
for (const candidate of Array.isArray(existingMessages) ? existingMessages : []) {
|
|
47
|
+
const existing = buildLiveMessageFingerprint(candidate);
|
|
48
|
+
if (incoming.id && existing.id && incoming.id === existing.id) return true;
|
|
49
|
+
if (incoming.role !== existing.role || incoming.text !== existing.text) continue;
|
|
50
|
+
if (incoming.sourceMessageId && existing.sourceMessageId && incoming.sourceMessageId === existing.sourceMessageId) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (!incoming.timestampMs || !existing.timestampMs) continue;
|
|
54
|
+
if (Math.abs(incoming.timestampMs - existing.timestampMs) <= timeWindowMs) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { escHtml, IS_SHARED_VIEW } from './store.js';
|
|
2
|
+
import { searchMessages } from './api-client.js';
|
|
3
|
+
|
|
4
|
+
const SEARCH_PAGE_SIZE = 40;
|
|
5
|
+
const SEARCH_DEBOUNCE_MS = 220;
|
|
6
|
+
|
|
7
|
+
const searchState = {
|
|
8
|
+
query: '',
|
|
9
|
+
results: [],
|
|
10
|
+
offset: 0,
|
|
11
|
+
hasMore: false,
|
|
12
|
+
loading: false,
|
|
13
|
+
error: '',
|
|
14
|
+
selectedKey: '',
|
|
15
|
+
scrollTop: 0,
|
|
16
|
+
requestSeq: 0,
|
|
17
|
+
debounceTimer: null,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let handlers = {
|
|
21
|
+
openConversation: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getSearchElements() {
|
|
25
|
+
return {
|
|
26
|
+
modal: document.getElementById('message-search-modal'),
|
|
27
|
+
input: document.getElementById('message-search-input'),
|
|
28
|
+
list: document.getElementById('message-search-results'),
|
|
29
|
+
status: document.getElementById('message-search-status'),
|
|
30
|
+
closeBtn: document.getElementById('message-search-close'),
|
|
31
|
+
clearBtn: document.getElementById('message-search-clear'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resultKey(result) {
|
|
36
|
+
const conversationId = String(result?.conversationId || '').trim();
|
|
37
|
+
const messageId = String(result?.messageId || '').trim();
|
|
38
|
+
return `${conversationId}:${messageId}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderSearchResults() {
|
|
42
|
+
const { list, status } = getSearchElements();
|
|
43
|
+
if (!list || !status) return;
|
|
44
|
+
status.textContent = searchState.error
|
|
45
|
+
|| (searchState.loading ? 'Searching…' : (searchState.results.length ? `${searchState.results.length} result${searchState.results.length === 1 ? '' : 's'}` : 'No results'));
|
|
46
|
+
if (!searchState.results.length) {
|
|
47
|
+
list.innerHTML = `<div class="message-search-empty">${escHtml(searchState.query.length < 2 ? 'Type at least 2 characters to search message history.' : (searchState.error || 'No matches found.'))}</div>`;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
list.innerHTML = searchState.results.map((item) => {
|
|
51
|
+
const key = resultKey(item);
|
|
52
|
+
const activeClass = key === searchState.selectedKey ? ' active' : '';
|
|
53
|
+
const title = String(item.conversationTitle || 'Conversation').trim();
|
|
54
|
+
const snippet = String(item.snippet || '').trim() || '(no text)';
|
|
55
|
+
const snippetHtml = escHtml(snippet)
|
|
56
|
+
.replaceAll('<mark>', '<mark>')
|
|
57
|
+
.replaceAll('</mark>', '</mark>');
|
|
58
|
+
const role = String(item.role || '').trim() || 'message';
|
|
59
|
+
const when = String(item.timestamp || '').trim() || '';
|
|
60
|
+
return `
|
|
61
|
+
<button type="button" class="message-search-result${activeClass}" data-search-key="${escHtml(key)}" data-search-conversation="${escHtml(item.conversationId)}" data-search-message="${escHtml(item.messageId)}">
|
|
62
|
+
<div class="message-search-result-title">${escHtml(title)}</div>
|
|
63
|
+
<div class="message-search-result-meta">${escHtml(role)}${when ? ` · ${escHtml(new Date(when).toLocaleString())}` : ''}</div>
|
|
64
|
+
<div class="message-search-result-snippet">${snippetHtml}</div>
|
|
65
|
+
</button>
|
|
66
|
+
`;
|
|
67
|
+
}).join('');
|
|
68
|
+
if (searchState.hasMore) {
|
|
69
|
+
list.insertAdjacentHTML('beforeend', '<div class="message-search-loading-more">Scroll for more…</div>');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function runMessageSearch({ reset = false } = {}) {
|
|
74
|
+
const { input } = getSearchElements();
|
|
75
|
+
const query = String(input?.value || searchState.query || '').trim();
|
|
76
|
+
if (reset) {
|
|
77
|
+
searchState.query = query;
|
|
78
|
+
searchState.offset = 0;
|
|
79
|
+
searchState.results = [];
|
|
80
|
+
searchState.hasMore = false;
|
|
81
|
+
searchState.error = '';
|
|
82
|
+
searchState.selectedKey = '';
|
|
83
|
+
renderSearchResults();
|
|
84
|
+
}
|
|
85
|
+
if (query.length < 2) {
|
|
86
|
+
searchState.loading = false;
|
|
87
|
+
renderSearchResults();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (searchState.loading) return;
|
|
91
|
+
searchState.loading = true;
|
|
92
|
+
renderSearchResults();
|
|
93
|
+
const reqId = ++searchState.requestSeq;
|
|
94
|
+
const result = await searchMessages({
|
|
95
|
+
query,
|
|
96
|
+
limit: SEARCH_PAGE_SIZE,
|
|
97
|
+
offset: reset ? 0 : searchState.offset,
|
|
98
|
+
});
|
|
99
|
+
if (reqId !== searchState.requestSeq) return;
|
|
100
|
+
searchState.loading = false;
|
|
101
|
+
if (!result) {
|
|
102
|
+
searchState.error = 'Search failed.';
|
|
103
|
+
renderSearchResults();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const incoming = Array.isArray(result.results) ? result.results : [];
|
|
107
|
+
if (reset) {
|
|
108
|
+
searchState.results = incoming;
|
|
109
|
+
} else {
|
|
110
|
+
const seen = new Set(searchState.results.map((item) => resultKey(item)));
|
|
111
|
+
for (const row of incoming) {
|
|
112
|
+
const key = resultKey(row);
|
|
113
|
+
if (!seen.has(key)) {
|
|
114
|
+
seen.add(key);
|
|
115
|
+
searchState.results.push(row);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
searchState.offset = Number(result?.pageInfo?.nextOffset ?? (searchState.results.length));
|
|
120
|
+
searchState.hasMore = !!result?.pageInfo?.hasMore;
|
|
121
|
+
searchState.error = '';
|
|
122
|
+
renderSearchResults();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function scheduleSearchDebounced() {
|
|
126
|
+
if (searchState.debounceTimer) {
|
|
127
|
+
window.clearTimeout(searchState.debounceTimer);
|
|
128
|
+
}
|
|
129
|
+
searchState.debounceTimer = window.setTimeout(() => {
|
|
130
|
+
searchState.debounceTimer = null;
|
|
131
|
+
void runMessageSearch({ reset: true });
|
|
132
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function openMessageSearchModal() {
|
|
136
|
+
if (IS_SHARED_VIEW) return;
|
|
137
|
+
const { modal, input, list } = getSearchElements();
|
|
138
|
+
if (!modal || !input || !list) return;
|
|
139
|
+
modal.classList.add('visible');
|
|
140
|
+
modal.setAttribute('aria-hidden', 'false');
|
|
141
|
+
input.value = searchState.query;
|
|
142
|
+
renderSearchResults();
|
|
143
|
+
requestAnimationFrame(() => {
|
|
144
|
+
list.scrollTop = searchState.scrollTop;
|
|
145
|
+
input.focus();
|
|
146
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function closeMessageSearchModal() {
|
|
151
|
+
const { modal, list } = getSearchElements();
|
|
152
|
+
if (!modal) return;
|
|
153
|
+
if (list) searchState.scrollTop = list.scrollTop;
|
|
154
|
+
modal.classList.remove('visible');
|
|
155
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function clearMessageSearchRuntimeState() {
|
|
159
|
+
searchState.query = '';
|
|
160
|
+
searchState.results = [];
|
|
161
|
+
searchState.offset = 0;
|
|
162
|
+
searchState.hasMore = false;
|
|
163
|
+
searchState.loading = false;
|
|
164
|
+
searchState.error = '';
|
|
165
|
+
searchState.selectedKey = '';
|
|
166
|
+
searchState.scrollTop = 0;
|
|
167
|
+
searchState.requestSeq += 1;
|
|
168
|
+
if (searchState.debounceTimer) {
|
|
169
|
+
window.clearTimeout(searchState.debounceTimer);
|
|
170
|
+
searchState.debounceTimer = null;
|
|
171
|
+
}
|
|
172
|
+
closeMessageSearchModal();
|
|
173
|
+
renderSearchResults();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function bindMessageSearchEvents() {
|
|
177
|
+
const { modal, input, list, closeBtn, clearBtn } = getSearchElements();
|
|
178
|
+
if (!modal || !input || !list || !closeBtn || !clearBtn) return;
|
|
179
|
+
if (modal.dataset.bound === '1') return;
|
|
180
|
+
modal.dataset.bound = '1';
|
|
181
|
+
|
|
182
|
+
closeBtn.addEventListener('click', (event) => {
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
closeMessageSearchModal();
|
|
185
|
+
});
|
|
186
|
+
clearBtn.addEventListener('click', (event) => {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
clearMessageSearchRuntimeState();
|
|
189
|
+
});
|
|
190
|
+
modal.addEventListener('click', (event) => {
|
|
191
|
+
if (event.target === modal) closeMessageSearchModal();
|
|
192
|
+
});
|
|
193
|
+
document.addEventListener('keydown', (event) => {
|
|
194
|
+
if (event.key !== 'Escape') return;
|
|
195
|
+
if (!modal.classList.contains('visible')) return;
|
|
196
|
+
closeMessageSearchModal();
|
|
197
|
+
});
|
|
198
|
+
input.addEventListener('input', () => {
|
|
199
|
+
scheduleSearchDebounced();
|
|
200
|
+
});
|
|
201
|
+
input.addEventListener('keydown', (event) => {
|
|
202
|
+
if (event.key === 'Enter') {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
void runMessageSearch({ reset: true });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
list.addEventListener('scroll', () => {
|
|
208
|
+
searchState.scrollTop = list.scrollTop;
|
|
209
|
+
const remaining = list.scrollHeight - list.clientHeight - list.scrollTop;
|
|
210
|
+
if (remaining <= 120 && searchState.hasMore && !searchState.loading) {
|
|
211
|
+
void runMessageSearch({ reset: false });
|
|
212
|
+
}
|
|
213
|
+
}, { passive: true });
|
|
214
|
+
list.addEventListener('click', (event) => {
|
|
215
|
+
const target = event.target instanceof Element ? event.target.closest('[data-search-conversation][data-search-message]') : null;
|
|
216
|
+
if (!target) return;
|
|
217
|
+
const conversationId = String(target.getAttribute('data-search-conversation') || '').trim();
|
|
218
|
+
const messageId = String(target.getAttribute('data-search-message') || '').trim();
|
|
219
|
+
if (!conversationId || !messageId) return;
|
|
220
|
+
searchState.selectedKey = String(target.getAttribute('data-search-key') || '').trim();
|
|
221
|
+
renderSearchResults();
|
|
222
|
+
closeMessageSearchModal();
|
|
223
|
+
void handlers.openConversation?.(conversationId, {
|
|
224
|
+
aroundMessageId: messageId,
|
|
225
|
+
focusMessageId: messageId,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function initMessageSearchView({ openConversation } = {}) {
|
|
231
|
+
handlers = {
|
|
232
|
+
openConversation: typeof openConversation === 'function' ? openConversation : null,
|
|
233
|
+
};
|
|
234
|
+
bindMessageSearchEvents();
|
|
235
|
+
renderSearchResults();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
openMessageSearchModal,
|
|
240
|
+
closeMessageSearchModal,
|
|
241
|
+
clearMessageSearchRuntimeState,
|
|
242
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function normalizeModelSelectorOptions(models = [], {
|
|
2
|
+
autoValue = 'auto',
|
|
3
|
+
labelFor = (modelId) => modelId,
|
|
4
|
+
} = {}) {
|
|
5
|
+
const normalizedAuto = String(autoValue || 'auto').trim() || 'auto';
|
|
6
|
+
const values = Array.from(new Set(
|
|
7
|
+
(Array.isArray(models) ? models : [])
|
|
8
|
+
.map((modelId) => String(modelId || '').trim())
|
|
9
|
+
.filter(Boolean),
|
|
10
|
+
)).filter((modelId) => modelId.toLowerCase() !== normalizedAuto.toLowerCase());
|
|
11
|
+
values.sort((left, right) => {
|
|
12
|
+
const labelOrder = String(labelFor(left) || left).localeCompare(
|
|
13
|
+
String(labelFor(right) || right),
|
|
14
|
+
undefined,
|
|
15
|
+
{ sensitivity: 'base', numeric: true },
|
|
16
|
+
);
|
|
17
|
+
return labelOrder || left.localeCompare(right);
|
|
18
|
+
});
|
|
19
|
+
return [
|
|
20
|
+
{ value: normalizedAuto, label: String(labelFor(normalizedAuto) || normalizedAuto) },
|
|
21
|
+
...values.map((value) => ({ value, label: String(labelFor(value) || value) })),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function modelSelectorOptionsEqual(currentOptions = [], nextOptions = []) {
|
|
26
|
+
return currentOptions.length === nextOptions.length
|
|
27
|
+
&& nextOptions.every((option, index) => (
|
|
28
|
+
currentOptions[index]?.value === option.value
|
|
29
|
+
&& currentOptions[index]?.label === option.label
|
|
30
|
+
));
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Advisory vision-capability heuristic for the composer.
|
|
2
|
+
//
|
|
3
|
+
// The browser only ever receives a list of model ID strings; the relay's model
|
|
4
|
+
// catalog does not carry `capabilities.supports.vision`. Until that metadata is
|
|
5
|
+
// plumbed through we cannot be authoritative, so this module is deliberately
|
|
6
|
+
// conservative: it warns only about models known to be text-only and stays
|
|
7
|
+
// silent whenever it does not recognise the id. The warning never blocks send.
|
|
8
|
+
|
|
9
|
+
const KNOWN_TEXT_ONLY_PATTERNS = [
|
|
10
|
+
/^o1-mini/,
|
|
11
|
+
/^o1-preview/,
|
|
12
|
+
/^text-embedding/,
|
|
13
|
+
/^codestral/,
|
|
14
|
+
/^code-davinci/,
|
|
15
|
+
/^text-davinci/,
|
|
16
|
+
/-instruct-text$/,
|
|
17
|
+
/^deepseek-coder/,
|
|
18
|
+
/^qwen[\w.-]*-coder/,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function normalizeModelId(modelId) {
|
|
22
|
+
return String(modelId || '').trim().toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns false only for models we positively recognise as text-only.
|
|
27
|
+
* Unknown ids are assumed vision-capable so the composer stays quiet rather
|
|
28
|
+
* than crying wolf on every model it has not heard of.
|
|
29
|
+
*/
|
|
30
|
+
export function modelLikelySupportsVision(modelId) {
|
|
31
|
+
const normalized = normalizeModelId(modelId);
|
|
32
|
+
if (!normalized) return true;
|
|
33
|
+
return !KNOWN_TEXT_ONLY_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function countImageAttachments(attachments = []) {
|
|
37
|
+
const list = Array.isArray(attachments) ? attachments : [];
|
|
38
|
+
return list.filter((attachment) => {
|
|
39
|
+
if (!attachment) return false;
|
|
40
|
+
if (typeof attachment.isImage === 'boolean') return attachment.isImage;
|
|
41
|
+
return String(attachment.type || '').toLowerCase().startsWith('image/');
|
|
42
|
+
}).length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function shouldWarnAboutImageAttachments(modelId, attachments = []) {
|
|
46
|
+
if (!countImageAttachments(attachments)) return false;
|
|
47
|
+
return !modelLikelySupportsVision(modelId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function imageAttachmentWarningText(modelId, attachments = []) {
|
|
51
|
+
if (!shouldWarnAboutImageAttachments(modelId, attachments)) return '';
|
|
52
|
+
const count = countImageAttachments(attachments);
|
|
53
|
+
const noun = count === 1 ? 'image' : 'images';
|
|
54
|
+
const label = String(modelId || '').trim() || 'the selected model';
|
|
55
|
+
return `${label} may not read ${noun} — the ${noun} will be sent as a file reference instead.`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function shouldPromptForNewConversationModel({ provider = '' } = {}) {
|
|
2
|
+
const normalizedProvider = String(provider || '').trim().toLowerCase();
|
|
3
|
+
return normalizedProvider === 'openai'
|
|
4
|
+
|| normalizedProvider === 'openai-byok'
|
|
5
|
+
|| normalizedProvider === 'openai-image'
|
|
6
|
+
|| normalizedProvider === 'openai-image-byok';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildNewConversationModelChoices(options = []) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const choices = [];
|
|
12
|
+
for (const option of Array.isArray(options) ? options : []) {
|
|
13
|
+
const value = String(option?.value || '').trim();
|
|
14
|
+
if (!value || option?.runtimeModelLock === true || seen.has(value)) continue;
|
|
15
|
+
seen.add(value);
|
|
16
|
+
choices.push({
|
|
17
|
+
value,
|
|
18
|
+
label: String(option?.label || value),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return choices;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeProviderKey(provider = '') {
|
|
25
|
+
const key = String(provider || '').trim().toLowerCase();
|
|
26
|
+
if (key === 'openai-byok') return 'openai';
|
|
27
|
+
if (key === 'github-copilot') return 'github';
|
|
28
|
+
return key;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeReasoningEfforts(efforts = []) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
for (const effort of Array.isArray(efforts) ? efforts : []) {
|
|
35
|
+
const value = String(effort || '').trim().toLowerCase();
|
|
36
|
+
if (!value || seen.has(value)) continue;
|
|
37
|
+
seen.add(value);
|
|
38
|
+
out.push(value);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function reasoningChoicesForProviderModel(catalog = {}, {
|
|
44
|
+
provider = '',
|
|
45
|
+
modelId = '',
|
|
46
|
+
} = {}) {
|
|
47
|
+
const normalizedModelId = String(modelId || '').trim().toLowerCase();
|
|
48
|
+
if (!normalizedModelId) return [];
|
|
49
|
+
const providerKey = normalizeProviderKey(provider);
|
|
50
|
+
const providerOptions = catalog?.reasoningByProvider?.[providerKey]?.[normalizedModelId];
|
|
51
|
+
if (Array.isArray(providerOptions) && providerOptions.length > 0) {
|
|
52
|
+
return normalizeReasoningEfforts(providerOptions);
|
|
53
|
+
}
|
|
54
|
+
return normalizeReasoningEfforts(catalog?.reasoningByModel?.[normalizedModelId] || []);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolvePreferredReasoningEffort(efforts = [], preferredValues = []) {
|
|
58
|
+
const normalizedEfforts = normalizeReasoningEfforts(efforts);
|
|
59
|
+
if (!normalizedEfforts.length) return '';
|
|
60
|
+
const candidates = Array.isArray(preferredValues) ? preferredValues : [preferredValues];
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
const value = String(candidate || '').trim().toLowerCase();
|
|
63
|
+
if (value && normalizedEfforts.includes(value)) return value;
|
|
64
|
+
}
|
|
65
|
+
const firstNonNone = normalizedEfforts.find((value) => value !== 'none');
|
|
66
|
+
return firstNonNone || normalizedEfforts[0];
|
|
67
|
+
}
|