@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,1082 @@
|
|
|
1
|
+
import { BASE, TOKEN, authHeaders, updateWorkspaceRootHints, applyContextUsageBar, readContextUsageRatio, currentConvId, conversations, setCliOnline, setCloudflaredTunnelState, setActiveRuntimeSessionCount, setRuntimeSessionBindingCount, setContextIndicatorMode, setServerPlatform } from './store.js';
|
|
2
|
+
|
|
3
|
+
// fetch() has no default timeout. Without one, a request issued while the app is
|
|
4
|
+
// being backgrounded can hang indefinitely on a half-open mobile connection and
|
|
5
|
+
// stall every caller awaiting it, including foreground recovery.
|
|
6
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
7
|
+
// Session bootstrap, uploads and git operations do real work server-side, so they
|
|
8
|
+
// get a ceiling that only catches a genuinely dead connection.
|
|
9
|
+
const LONG_REQUEST_TIMEOUT_MS = 120000;
|
|
10
|
+
|
|
11
|
+
let networkRequestsEnabled = true;
|
|
12
|
+
let fetchOutageActive = false;
|
|
13
|
+
let lastFetchOutageSignature = '';
|
|
14
|
+
|
|
15
|
+
function requestTimeoutSignal(timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
16
|
+
return AbortSignal.timeout?.(timeoutMs);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toErrorMessage(error) {
|
|
20
|
+
if (!error) return 'unknown error';
|
|
21
|
+
const message = String(error?.message || error || '').trim();
|
|
22
|
+
return message || 'unknown error';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function noteFetchSuccess() {
|
|
26
|
+
fetchOutageActive = false;
|
|
27
|
+
lastFetchOutageSignature = '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function noteFetchFailure(url, error) {
|
|
31
|
+
if (!networkRequestsEnabled) return;
|
|
32
|
+
const signature = `${String(url || '').trim()}::${toErrorMessage(error)}`;
|
|
33
|
+
if (fetchOutageActive && signature === lastFetchOutageSignature) return;
|
|
34
|
+
fetchOutageActive = true;
|
|
35
|
+
lastFetchOutageSignature = signature;
|
|
36
|
+
console.error('Fetch error', toErrorMessage(error));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setNetworkRequestsEnabled(value) {
|
|
40
|
+
networkRequestsEnabled = !!value;
|
|
41
|
+
if (networkRequestsEnabled) noteFetchSuccess();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function areNetworkRequestsEnabled() {
|
|
45
|
+
return networkRequestsEnabled;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function apiFetch(url, opts = {}) {
|
|
49
|
+
if (!networkRequestsEnabled) return null;
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(`${BASE}${url}`, {
|
|
52
|
+
signal: requestTimeoutSignal(),
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
...authHeaders(),
|
|
56
|
+
...(opts.headers || {}),
|
|
57
|
+
},
|
|
58
|
+
...opts,
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
console.error('API error', response.status, url);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
noteFetchSuccess();
|
|
65
|
+
return response.json();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
noteFetchFailure(url, error);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function verifyExistingSession(tokenCandidate = '') {
|
|
73
|
+
if (!networkRequestsEnabled) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
status: 0,
|
|
77
|
+
payload: null,
|
|
78
|
+
error: 'Network requests are paused',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const requestStatus = async (headers = null) => {
|
|
83
|
+
const response = await fetch(`${BASE}/api/status`, {
|
|
84
|
+
signal: requestTimeoutSignal(),
|
|
85
|
+
...(headers ? { headers } : {}),
|
|
86
|
+
});
|
|
87
|
+
const payload = await response.json().catch(() => null);
|
|
88
|
+
if (response.ok && payload) {
|
|
89
|
+
updateWorkspaceRootHints(payload);
|
|
90
|
+
setContextIndicatorMode(payload?.contextIndicatorMode);
|
|
91
|
+
setCliOnline(!!payload?.cliOnline);
|
|
92
|
+
setCloudflaredTunnelState(payload?.cloudflaredTunnel || null);
|
|
93
|
+
setActiveRuntimeSessionCount(payload?.activeRuntimeSessionCount);
|
|
94
|
+
setRuntimeSessionBindingCount(payload?.runtimeSessionBindingCount);
|
|
95
|
+
if (payload?.platform) setServerPlatform(payload.platform);
|
|
96
|
+
}
|
|
97
|
+
if (response.ok) noteFetchSuccess();
|
|
98
|
+
return {
|
|
99
|
+
ok: response.ok,
|
|
100
|
+
status: response.status,
|
|
101
|
+
payload,
|
|
102
|
+
error: response.ok ? null : (payload?.error || 'Unauthorized'),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const cookieResult = await requestStatus();
|
|
107
|
+
if (cookieResult.ok) return cookieResult;
|
|
108
|
+
const normalizedToken = String(tokenCandidate || '').trim();
|
|
109
|
+
if (!normalizedToken) return cookieResult;
|
|
110
|
+
if (cookieResult.status !== 401) return cookieResult;
|
|
111
|
+
const tokenResult = await requestStatus({ Authorization: `Bearer ${normalizedToken}` });
|
|
112
|
+
if (tokenResult?.ok) {
|
|
113
|
+
return {
|
|
114
|
+
...tokenResult,
|
|
115
|
+
source: 'token',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return tokenResult;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
noteFetchFailure('/api/status', error);
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
status: 0,
|
|
124
|
+
payload: null,
|
|
125
|
+
error: error?.message || 'Network error',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function verifyToken(token) {
|
|
131
|
+
if (!networkRequestsEnabled) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
status: 0,
|
|
135
|
+
payload: null,
|
|
136
|
+
error: 'Network requests are paused',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(`${BASE}/api/status`, {
|
|
141
|
+
signal: requestTimeoutSignal(),
|
|
142
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
143
|
+
});
|
|
144
|
+
const payload = await response.json().catch(() => null);
|
|
145
|
+
if (response.ok && payload) {
|
|
146
|
+
updateWorkspaceRootHints(payload);
|
|
147
|
+
setContextIndicatorMode(payload?.contextIndicatorMode);
|
|
148
|
+
setCliOnline(!!payload?.cliOnline);
|
|
149
|
+
setCloudflaredTunnelState(payload?.cloudflaredTunnel || null);
|
|
150
|
+
setActiveRuntimeSessionCount(payload?.activeRuntimeSessionCount);
|
|
151
|
+
setRuntimeSessionBindingCount(payload?.runtimeSessionBindingCount);
|
|
152
|
+
if (payload?.platform) setServerPlatform(payload.platform);
|
|
153
|
+
}
|
|
154
|
+
if (response.ok) noteFetchSuccess();
|
|
155
|
+
return {
|
|
156
|
+
ok: response.ok,
|
|
157
|
+
status: response.status,
|
|
158
|
+
payload,
|
|
159
|
+
error: response.ok ? null : (payload?.error || 'Unauthorized'),
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
noteFetchFailure('/api/status', error);
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
status: 0,
|
|
166
|
+
payload: null,
|
|
167
|
+
error: error?.message || 'Network error',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function refreshWorkspaceRootHints() {
|
|
173
|
+
const status = await apiFetch('/api/status');
|
|
174
|
+
if (status) {
|
|
175
|
+
updateWorkspaceRootHints(status);
|
|
176
|
+
setContextIndicatorMode(status?.contextIndicatorMode);
|
|
177
|
+
setCliOnline(!!status?.cliOnline);
|
|
178
|
+
setCloudflaredTunnelState(status?.cloudflaredTunnel || null);
|
|
179
|
+
setActiveRuntimeSessionCount(status?.activeRuntimeSessionCount);
|
|
180
|
+
setRuntimeSessionBindingCount(status?.runtimeSessionBindingCount);
|
|
181
|
+
if (status?.platform) setServerPlatform(status.platform);
|
|
182
|
+
}
|
|
183
|
+
return status;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function updateWorkspaceRoot(rootPath, conversationId = null) {
|
|
187
|
+
const pathValue = String(rootPath || '').trim();
|
|
188
|
+
if (!pathValue) return null;
|
|
189
|
+
const convId = String(conversationId || '').trim();
|
|
190
|
+
const endpoint = convId
|
|
191
|
+
? `/api/conversation/${encodeURIComponent(convId)}/workspace-root`
|
|
192
|
+
: '/api/workspace-root';
|
|
193
|
+
const response = await apiFetch(endpoint, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
body: JSON.stringify({ rootPath: pathValue }),
|
|
196
|
+
});
|
|
197
|
+
if (response && !convId) updateWorkspaceRootHints(response);
|
|
198
|
+
return response;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function updateDefaultSessionWorkspaceRoot(rootPath, options = {}) {
|
|
202
|
+
const clear = options?.clear === true;
|
|
203
|
+
const pathValue = String(rootPath || '').trim();
|
|
204
|
+
const response = await apiFetch('/api/settings/default-session-workspace-root', {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
rootPath: clear ? '' : pathValue,
|
|
208
|
+
clear,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
if (response) updateWorkspaceRootHints(response);
|
|
212
|
+
return response;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function loadOpenAISettings() {
|
|
216
|
+
return apiFetch('/api/settings/openai');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A settings POST that surfaces the relay's own error string.
|
|
221
|
+
*
|
|
222
|
+
* `apiFetch` answers a non-2xx with null, which is the right default for reads
|
|
223
|
+
* but throws away the one thing these endpoints exist to tell you: WHY the save
|
|
224
|
+
* was refused ("the SDK engine requires session worker routing…", "logout
|
|
225
|
+
* refused while a login is in flight"). Every settings mutation wants the same
|
|
226
|
+
* explicit shape, so it lives here once instead of being re-typed per provider.
|
|
227
|
+
*
|
|
228
|
+
* Throws on failure — callers render `error.message` — and returns null only
|
|
229
|
+
* when network requests are disabled outright.
|
|
230
|
+
*/
|
|
231
|
+
async function settingsRequest(path, body = null, fallbackError = 'Request failed') {
|
|
232
|
+
if (!networkRequestsEnabled) return null;
|
|
233
|
+
try {
|
|
234
|
+
const response = await fetch(`${BASE}${path}`, {
|
|
235
|
+
signal: requestTimeoutSignal(),
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
'Content-Type': 'application/json',
|
|
239
|
+
...authHeaders(),
|
|
240
|
+
},
|
|
241
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
242
|
+
});
|
|
243
|
+
const result = await response.json().catch(() => null);
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
throw new Error(String(result?.error || `${fallbackError} (${response.status})`).trim());
|
|
246
|
+
}
|
|
247
|
+
noteFetchSuccess();
|
|
248
|
+
return result;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
noteFetchFailure(path, error);
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function loadCopilotSettings() {
|
|
256
|
+
return apiFetch('/api/settings/copilot');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function updateCopilotSettings({ engine = undefined } = {}) {
|
|
260
|
+
const payload = {};
|
|
261
|
+
if (typeof engine === 'string' && engine.trim()) payload.engine = engine.trim();
|
|
262
|
+
return settingsRequest('/api/settings/copilot', payload, 'Failed to update Copilot settings');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function loadClaudeSettings() {
|
|
266
|
+
return apiFetch('/api/settings/claude');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function updateClaudeSettings({
|
|
270
|
+
enabled = undefined,
|
|
271
|
+
model = undefined,
|
|
272
|
+
enabledModels = undefined,
|
|
273
|
+
} = {}) {
|
|
274
|
+
const payload = {};
|
|
275
|
+
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
276
|
+
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
277
|
+
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
278
|
+
return settingsRequest('/api/settings/claude', payload, 'Failed to update Claude settings');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Claude account auth (Relogin / Logout). The relay answers failures with a
|
|
282
|
+
// useful `error` string (e.g. logout refused with 409 while a login is in
|
|
283
|
+
// flight), which is exactly what `settingsRequest` preserves.
|
|
284
|
+
function claudeAuthRequest(path, body = null) {
|
|
285
|
+
return settingsRequest(path, body, 'Claude auth request failed');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function getClaudeAuthStatus() {
|
|
289
|
+
return apiFetch('/api/claude/auth/status');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function startClaudeLogin() {
|
|
293
|
+
return claudeAuthRequest('/api/claude/auth/login/start');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// The pasted code is sent once and never stored, logged, or echoed back.
|
|
297
|
+
export async function submitClaudeLoginCode(code) {
|
|
298
|
+
return claudeAuthRequest('/api/claude/auth/login/code', { code: String(code || '') });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function cancelClaudeLogin() {
|
|
302
|
+
return claudeAuthRequest('/api/claude/auth/login/cancel');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function claudeLogout() {
|
|
306
|
+
return claudeAuthRequest('/api/claude/auth/logout');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Provider CLI install / update. The body only ever carries a descriptor id and
|
|
310
|
+
// an action name — the commands themselves are frozen literals in server code —
|
|
311
|
+
// so there is nothing here for a caller to interpolate into a shell.
|
|
312
|
+
export async function getCliStatus({ force = false } = {}) {
|
|
313
|
+
// Each probe spawns short-lived CLI processes on the host, so a refresh is
|
|
314
|
+
// opt-in: the settings panel asks for one when it opens, sockets do the rest.
|
|
315
|
+
return apiFetch(`/api/cli/status${force ? '?force=1' : ''}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function startCliInstall(provider, action) {
|
|
319
|
+
return settingsRequest(
|
|
320
|
+
'/api/cli/install',
|
|
321
|
+
{ provider: String(provider || ''), action: String(action || '') },
|
|
322
|
+
'Failed to start the CLI install',
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Kills a running install, and doubles as the dismiss path for a settled one.
|
|
327
|
+
export async function cancelCliInstall() {
|
|
328
|
+
return settingsRequest('/api/cli/install/cancel', null, 'Failed to cancel the CLI install');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Grok account auth (Sign in / Sign out). One route shorter than Claude's: the
|
|
332
|
+
// device code rides in the authorize URL and the CLI polls x.ai itself, so
|
|
333
|
+
// nothing is ever pasted back through the relay.
|
|
334
|
+
function grokAuthRequest(path, body = null) {
|
|
335
|
+
return settingsRequest(path, body, 'Grok auth request failed');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function getGrokAuthStatus() {
|
|
339
|
+
return apiFetch('/api/grok/auth/status');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function startGrokLogin() {
|
|
343
|
+
return grokAuthRequest('/api/grok/auth/login/start');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function cancelGrokLogin() {
|
|
347
|
+
return grokAuthRequest('/api/grok/auth/login/cancel');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function grokLogout() {
|
|
351
|
+
return grokAuthRequest('/api/grok/auth/logout');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function loadGrokSettings() {
|
|
355
|
+
return apiFetch('/api/settings/grok');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function updateGrokSettings({
|
|
359
|
+
enabled = undefined,
|
|
360
|
+
model = undefined,
|
|
361
|
+
enabledModels = undefined,
|
|
362
|
+
} = {}) {
|
|
363
|
+
const payload = {};
|
|
364
|
+
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
365
|
+
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
366
|
+
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
367
|
+
return settingsRequest('/api/settings/grok', payload, 'Failed to update Grok settings');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function updateOpenAISettings({
|
|
371
|
+
apiKey = '',
|
|
372
|
+
model = 'gpt-4o',
|
|
373
|
+
baseUrl = undefined,
|
|
374
|
+
enabled = undefined,
|
|
375
|
+
remove = false,
|
|
376
|
+
} = {}) {
|
|
377
|
+
const payload = {
|
|
378
|
+
apiKey: String(apiKey || '').trim(),
|
|
379
|
+
model: String(model || '').trim() || 'gpt-4o',
|
|
380
|
+
remove: remove === true,
|
|
381
|
+
};
|
|
382
|
+
if (typeof baseUrl === 'string') payload.baseUrl = String(baseUrl).trim();
|
|
383
|
+
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
384
|
+
return settingsRequest('/api/settings/openai', payload, 'Failed to update OpenAI settings');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export async function loadCursorSettings() {
|
|
388
|
+
return apiFetch('/api/settings/cursor');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function updateCursorSettings({
|
|
392
|
+
apiKey = '',
|
|
393
|
+
model = undefined,
|
|
394
|
+
enabled = undefined,
|
|
395
|
+
enabledModels = undefined,
|
|
396
|
+
remove = false,
|
|
397
|
+
} = {}) {
|
|
398
|
+
const payload = {
|
|
399
|
+
apiKey: String(apiKey || '').trim(),
|
|
400
|
+
remove: remove === true,
|
|
401
|
+
};
|
|
402
|
+
// Only send the model when the caller chose one: an enabled-models-only save
|
|
403
|
+
// must not reset the configured default model.
|
|
404
|
+
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
405
|
+
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
406
|
+
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
407
|
+
return settingsRequest('/api/settings/cursor', payload, 'Failed to update Cursor settings');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function loadWindowsAutostartSetting() {
|
|
411
|
+
return apiFetch('/api/settings/windows-autostart');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function updateWindowsAutostartSetting(enabled) {
|
|
415
|
+
return apiFetch('/api/settings/windows-autostart', {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
body: JSON.stringify({ enabled: !!enabled }),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function loadTurnCeilingSetting() {
|
|
422
|
+
return apiFetch('/api/settings/turn-ceiling');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function updateTurnCeilingSetting(ceilingMinutes) {
|
|
426
|
+
return apiFetch('/api/settings/turn-ceiling', {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
body: JSON.stringify({ ceilingMinutes: Number(ceilingMinutes) }),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function loadBackgroundTaskTimeoutSetting() {
|
|
433
|
+
return apiFetch('/api/settings/background-task-timeout');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export async function updateBackgroundTaskTimeoutSetting(timeoutMinutes) {
|
|
437
|
+
return apiFetch('/api/settings/background-task-timeout', {
|
|
438
|
+
method: 'POST',
|
|
439
|
+
body: JSON.stringify({ timeoutMinutes: Number(timeoutMinutes) }),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function launchSessionWorker(sdkSessionId) {
|
|
444
|
+
const sessionId = String(sdkSessionId || '').trim();
|
|
445
|
+
if (!sessionId) return null;
|
|
446
|
+
return apiFetch(`/api/session-worker/${encodeURIComponent(sessionId)}/launch`, {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function relaunchSessionWorkerWithWorkspaceRoot(conversationId, rootPath, idempotencyKey = '') {
|
|
452
|
+
const convId = String(conversationId || '').trim();
|
|
453
|
+
const pathValue = String(rootPath || '').trim();
|
|
454
|
+
if (!convId || !pathValue) return null;
|
|
455
|
+
// One key per user gesture, so a duplicate delivery (retried fetch, second tab,
|
|
456
|
+
// stray touch event) is provably the same request and the relay replays it
|
|
457
|
+
// instead of running a second stop/spawn cycle.
|
|
458
|
+
return apiFetch(`/api/conversation/${encodeURIComponent(convId)}/relaunch-with-workspace-root`, {
|
|
459
|
+
method: 'POST',
|
|
460
|
+
body: JSON.stringify({ rootPath: pathValue, idempotencyKey: String(idempotencyKey || '') }),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export async function loadUsageSummary() {
|
|
465
|
+
return apiFetch('/api/usage');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function loadCursorAllowanceSettings() {
|
|
469
|
+
return apiFetch('/api/settings/cursor-allowance');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function updateCursorAllowanceSettings(payload = {}) {
|
|
473
|
+
return apiFetch('/api/settings/cursor-allowance', {
|
|
474
|
+
method: 'POST',
|
|
475
|
+
body: JSON.stringify(payload),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export async function loadCursorDashboardTokenSettings() {
|
|
480
|
+
return apiFetch('/api/settings/cursor-dashboard-token');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export async function updateCursorDashboardTokenSettings(payload = {}) {
|
|
484
|
+
return apiFetch('/api/settings/cursor-dashboard-token', {
|
|
485
|
+
method: 'POST',
|
|
486
|
+
body: JSON.stringify(payload),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export async function loadGrokAllowanceSettings() {
|
|
491
|
+
return apiFetch('/api/settings/grok-allowance');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function updateGrokAllowanceSettings(payload = {}) {
|
|
495
|
+
return apiFetch('/api/settings/grok-allowance', {
|
|
496
|
+
method: 'POST',
|
|
497
|
+
body: JSON.stringify(payload),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export async function loadContextSummary(convId = null) {
|
|
502
|
+
const trimmedConvId = String(convId || '').trim();
|
|
503
|
+
const lookupId = resolveContextLookupId(trimmedConvId);
|
|
504
|
+
const endpoint = lookupId ? `/api/context/${encodeURIComponent(lookupId)}` : '/api/context';
|
|
505
|
+
return apiFetch(endpoint);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function resolveContextLookupId(conversationId) {
|
|
509
|
+
const convId = String(conversationId || '').trim();
|
|
510
|
+
if (!convId) return '';
|
|
511
|
+
const conversation = conversations[convId];
|
|
512
|
+
const sdkSessionId = String(conversation?.sdkSessionId || conversation?.sdk_session_id || '').trim();
|
|
513
|
+
return sdkSessionId || convId;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export async function loadModelCatalog() {
|
|
517
|
+
return apiFetch('/api/models');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export async function loadModelVariantCatalog() {
|
|
521
|
+
return apiFetch('/api/model-variants');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export async function refreshModelVariantCatalog() {
|
|
525
|
+
return apiFetch('/api/model-variants/refresh', { method: 'POST' });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export async function saveEnabledModelVariants(enabledVariantIds = []) {
|
|
529
|
+
return apiFetch('/api/model-variants', {
|
|
530
|
+
method: 'PATCH',
|
|
531
|
+
body: JSON.stringify({
|
|
532
|
+
enabledVariantIds: Array.isArray(enabledVariantIds)
|
|
533
|
+
? enabledVariantIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
534
|
+
: [],
|
|
535
|
+
}),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export async function loadConversations(options = {}) {
|
|
540
|
+
const params = new URLSearchParams();
|
|
541
|
+
const limit = Math.max(1, Math.trunc(Number(options.limit) || 0));
|
|
542
|
+
if (limit) params.set('limit', String(limit));
|
|
543
|
+
const beforeConversationId = String(options.beforeConversationId || options.beforeId || '').trim();
|
|
544
|
+
if (beforeConversationId) params.set('beforeConversationId', beforeConversationId);
|
|
545
|
+
const beforeUpdatedAt = String(options.beforeUpdatedAt || '').trim();
|
|
546
|
+
if (beforeUpdatedAt) params.set('beforeUpdatedAt', beforeUpdatedAt);
|
|
547
|
+
const includeArchived = String(options.archived || '').trim().toLowerCase();
|
|
548
|
+
if (includeArchived === 'true') params.set('archived', 'true');
|
|
549
|
+
const query = params.toString();
|
|
550
|
+
return apiFetch(`/api/conversations${query ? `?${query}` : ''}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function loadConversation(id, options = {}) {
|
|
554
|
+
const convId = String(id || '').trim();
|
|
555
|
+
if (!convId) return null;
|
|
556
|
+
const params = new URLSearchParams();
|
|
557
|
+
const limit = Math.max(1, Math.trunc(Number(options.limit) || 0));
|
|
558
|
+
if (limit) params.set('limit', String(limit));
|
|
559
|
+
const beforeMessageId = String(options.beforeMessageId || options.before || '').trim();
|
|
560
|
+
if (beforeMessageId) params.set('beforeMessageId', beforeMessageId);
|
|
561
|
+
const beforeTimestamp = String(options.beforeTimestamp || '').trim();
|
|
562
|
+
if (beforeTimestamp) params.set('beforeTimestamp', beforeTimestamp);
|
|
563
|
+
const afterMessageId = String(options.afterMessageId || options.after || '').trim();
|
|
564
|
+
if (afterMessageId) params.set('afterMessageId', afterMessageId);
|
|
565
|
+
const afterTimestamp = String(options.afterTimestamp || '').trim();
|
|
566
|
+
if (afterTimestamp) params.set('afterTimestamp', afterTimestamp);
|
|
567
|
+
const aroundMessageId = String(options.aroundMessageId || '').trim();
|
|
568
|
+
if (aroundMessageId) params.set('aroundMessageId', aroundMessageId);
|
|
569
|
+
const query = params.toString();
|
|
570
|
+
return apiFetch(`/api/conversation/${convId}${query ? `?${query}` : ''}`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export async function refreshConversationHistory(id, options = {}) {
|
|
574
|
+
const convId = String(id || '').trim();
|
|
575
|
+
if (!convId) return null;
|
|
576
|
+
const params = new URLSearchParams();
|
|
577
|
+
const limit = Math.max(1, Math.trunc(Number(options.limit) || 0));
|
|
578
|
+
if (limit) params.set('limit', String(limit));
|
|
579
|
+
const query = params.toString();
|
|
580
|
+
return apiFetch(`/api/conversation/${convId}/refresh-history${query ? `?${query}` : ''}`, {
|
|
581
|
+
method: 'POST',
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export async function searchMessages(options = {}) {
|
|
586
|
+
const query = String(options.query || options.q || '').trim();
|
|
587
|
+
if (!query) return null;
|
|
588
|
+
const params = new URLSearchParams();
|
|
589
|
+
params.set('q', query);
|
|
590
|
+
const limit = Math.max(1, Math.trunc(Number(options.limit) || 0));
|
|
591
|
+
if (limit) params.set('limit', String(limit));
|
|
592
|
+
const offset = Math.max(0, Math.trunc(Number(options.offset) || 0));
|
|
593
|
+
if (offset) params.set('offset', String(offset));
|
|
594
|
+
return apiFetch(`/api/search/messages?${params.toString()}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export async function deleteConversation(id) {
|
|
598
|
+
const convId = String(id || '').trim();
|
|
599
|
+
if (!convId) return null;
|
|
600
|
+
return apiFetch(`/api/conversation/${convId}`, { method: 'DELETE' });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function updateMessageShareVisibility(conversationId, messageId, hiddenFromShares) {
|
|
604
|
+
const convId = String(conversationId || '').trim();
|
|
605
|
+
const msgId = String(messageId || '').trim();
|
|
606
|
+
if (!convId || !msgId) return null;
|
|
607
|
+
return apiFetch(`/api/conversation/${encodeURIComponent(convId)}/message/${encodeURIComponent(msgId)}/share-visibility`, {
|
|
608
|
+
method: 'PATCH',
|
|
609
|
+
body: JSON.stringify({ hiddenFromShares: hiddenFromShares === true }),
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export async function updateConversationTitle(id, title) {
|
|
614
|
+
const convId = String(id || '').trim();
|
|
615
|
+
if (!convId) return null;
|
|
616
|
+
return apiFetch(`/api/conversation/${convId}`, {
|
|
617
|
+
method: 'PATCH',
|
|
618
|
+
body: JSON.stringify({ title }),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export async function updateConversationPreferences(id, preferences = {}) {
|
|
623
|
+
const convId = String(id || '').trim();
|
|
624
|
+
if (!convId) return null;
|
|
625
|
+
return apiFetch(`/api/conversation/${convId}/preferences`, {
|
|
626
|
+
method: 'PATCH',
|
|
627
|
+
body: JSON.stringify(preferences || {}),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export async function createConversationShareLink(id) {
|
|
632
|
+
const convId = String(id || '').trim();
|
|
633
|
+
if (!convId) return null;
|
|
634
|
+
return apiFetch(`/api/conversation/${encodeURIComponent(convId)}/share`, {
|
|
635
|
+
method: 'POST',
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export async function loadServerStatusEventPage({ before = null, limit = 40 } = {}) {
|
|
640
|
+
const params = new URLSearchParams();
|
|
641
|
+
const pageSize = Math.max(1, Math.min(100, Math.trunc(Number(limit) || 40)));
|
|
642
|
+
params.set('limit', String(pageSize));
|
|
643
|
+
const beforeTimestamp = Number(before?.timestamp);
|
|
644
|
+
const beforeId = String(before?.id || '').trim();
|
|
645
|
+
if (Number.isFinite(beforeTimestamp) && beforeId) {
|
|
646
|
+
params.set('beforeTimestamp', String(Math.trunc(beforeTimestamp)));
|
|
647
|
+
params.set('beforeId', beforeId);
|
|
648
|
+
}
|
|
649
|
+
const payload = await apiFetch(`/api/status/events?${params.toString()}`);
|
|
650
|
+
return {
|
|
651
|
+
items: Array.isArray(payload?.items) ? payload.items : [],
|
|
652
|
+
hasMore: payload?.hasMore === true,
|
|
653
|
+
nextCursor: payload?.nextCursor || null,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export async function loadSharedConversation(shareToken, options = {}) {
|
|
658
|
+
const token = String(shareToken || '').trim().toLowerCase();
|
|
659
|
+
if (!token) return { ok: false, status: 400, error: 'Missing shared token' };
|
|
660
|
+
const params = new URLSearchParams();
|
|
661
|
+
const limit = Math.max(1, Math.trunc(Number(options.limit) || 0));
|
|
662
|
+
if (limit) params.set('limit', String(limit));
|
|
663
|
+
const beforeMessageId = String(options.beforeMessageId || options.before || '').trim();
|
|
664
|
+
if (beforeMessageId) params.set('beforeMessageId', beforeMessageId);
|
|
665
|
+
const beforeTimestamp = String(options.beforeTimestamp || '').trim();
|
|
666
|
+
if (beforeTimestamp) params.set('beforeTimestamp', beforeTimestamp);
|
|
667
|
+
const afterMessageId = String(options.afterMessageId || options.after || '').trim();
|
|
668
|
+
if (afterMessageId) params.set('afterMessageId', afterMessageId);
|
|
669
|
+
const afterTimestamp = String(options.afterTimestamp || '').trim();
|
|
670
|
+
if (afterTimestamp) params.set('afterTimestamp', afterTimestamp);
|
|
671
|
+
const aroundMessageId = String(options.aroundMessageId || '').trim();
|
|
672
|
+
if (aroundMessageId) params.set('aroundMessageId', aroundMessageId);
|
|
673
|
+
const query = params.toString();
|
|
674
|
+
try {
|
|
675
|
+
const response = await fetch(`${BASE}/api/shared/${encodeURIComponent(token)}${query ? `?${query}` : ''}`, {
|
|
676
|
+
signal: requestTimeoutSignal(),
|
|
677
|
+
headers: { 'Content-Type': 'application/json' },
|
|
678
|
+
});
|
|
679
|
+
const payload = await response.json().catch(() => null);
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
return {
|
|
682
|
+
ok: false,
|
|
683
|
+
status: response.status,
|
|
684
|
+
error: payload?.error || `Shared conversation request failed (${response.status})`,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
ok: true,
|
|
689
|
+
status: response.status,
|
|
690
|
+
...(payload && typeof payload === 'object' ? payload : {}),
|
|
691
|
+
};
|
|
692
|
+
} catch (error) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
status: 0,
|
|
696
|
+
error: error?.message || 'Network error',
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export async function reportSharedViewerPresence(shareToken, viewerId) {
|
|
702
|
+
const token = String(shareToken || '').trim().toLowerCase();
|
|
703
|
+
const viewer = String(viewerId || '').trim();
|
|
704
|
+
if (!token || !viewer) return { ok: false, status: 400, error: 'Missing shared presence payload' };
|
|
705
|
+
try {
|
|
706
|
+
const response = await fetch(`${BASE}/api/shared/${encodeURIComponent(token)}/presence`, {
|
|
707
|
+
signal: requestTimeoutSignal(),
|
|
708
|
+
method: 'POST',
|
|
709
|
+
headers: { 'Content-Type': 'application/json' },
|
|
710
|
+
body: JSON.stringify({ viewerId: viewer }),
|
|
711
|
+
});
|
|
712
|
+
const payload = await response.json().catch(() => null);
|
|
713
|
+
if (!response.ok) {
|
|
714
|
+
return {
|
|
715
|
+
ok: false,
|
|
716
|
+
status: response.status,
|
|
717
|
+
error: payload?.error || `Shared presence request failed (${response.status})`,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
ok: true,
|
|
722
|
+
status: response.status,
|
|
723
|
+
...(payload && typeof payload === 'object' ? payload : {}),
|
|
724
|
+
};
|
|
725
|
+
} catch (error) {
|
|
726
|
+
return {
|
|
727
|
+
ok: false,
|
|
728
|
+
status: 0,
|
|
729
|
+
error: error?.message || 'Network error',
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export async function updateConversationDraft(id, draft = {}) {
|
|
735
|
+
const convId = String(id || '').trim();
|
|
736
|
+
if (!convId) return null;
|
|
737
|
+
try {
|
|
738
|
+
const response = await fetch(`${BASE}/api/conversation/${convId}/draft`, {
|
|
739
|
+
signal: requestTimeoutSignal(),
|
|
740
|
+
method: 'PATCH',
|
|
741
|
+
headers: {
|
|
742
|
+
'Content-Type': 'application/json',
|
|
743
|
+
...authHeaders(),
|
|
744
|
+
},
|
|
745
|
+
body: JSON.stringify(draft || {}),
|
|
746
|
+
});
|
|
747
|
+
const payload = await response.json().catch(() => null);
|
|
748
|
+
if (response.ok) return payload;
|
|
749
|
+
return {
|
|
750
|
+
ok: false,
|
|
751
|
+
status: response.status,
|
|
752
|
+
...(payload && typeof payload === 'object' ? payload : {}),
|
|
753
|
+
};
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.error('Fetch error', error);
|
|
756
|
+
return { ok: false, error: error?.message || 'Network error' };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export async function compactConversation(id) {
|
|
761
|
+
const convId = String(id || '').trim();
|
|
762
|
+
if (!convId) return null;
|
|
763
|
+
return apiFetch(`/api/conversation/${convId}/compact`, { method: 'POST' });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export async function killSessionWorker(sdkSessionId, body = {}) {
|
|
767
|
+
const sessionId = String(sdkSessionId || '').trim();
|
|
768
|
+
if (!sessionId) return null;
|
|
769
|
+
return apiFetch(`/api/session-worker/${encodeURIComponent(sessionId)}/kill`, {
|
|
770
|
+
method: 'POST',
|
|
771
|
+
body: JSON.stringify(body || {}),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export async function requestRelayRestart(body = {}) {
|
|
776
|
+
return apiFetch('/api/relay/shutdown', {
|
|
777
|
+
method: 'POST',
|
|
778
|
+
body: JSON.stringify({
|
|
779
|
+
reason: 'manual-restart',
|
|
780
|
+
requestedBy: 'localhost-api',
|
|
781
|
+
restart: true,
|
|
782
|
+
...(body || {}),
|
|
783
|
+
}),
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export async function requestHostSuspend(body = {}) {
|
|
788
|
+
return apiFetch('/api/host/suspend', {
|
|
789
|
+
method: 'POST',
|
|
790
|
+
body: JSON.stringify({
|
|
791
|
+
reason: 'manual-suspend',
|
|
792
|
+
requestedBy: 'localhost-api',
|
|
793
|
+
...(body || {}),
|
|
794
|
+
}),
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export async function requestQueueEmpty(body = {}) {
|
|
799
|
+
return apiFetch('/api/queue/empty', {
|
|
800
|
+
method: 'POST',
|
|
801
|
+
body: JSON.stringify({
|
|
802
|
+
reason: 'manual-empty-queue',
|
|
803
|
+
requestedBy: 'localhost-api',
|
|
804
|
+
...(body || {}),
|
|
805
|
+
}),
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export async function sendMessage(body) {
|
|
810
|
+
return apiFetch('/api/message', { method: 'POST', body: JSON.stringify(body) });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Raw fetch instead of apiFetch: a rejected bootstrap (bad CWD, unavailable
|
|
814
|
+
// model) must surface the server's error message so the New Chat modal can
|
|
815
|
+
// show it and keep the user's selection editable.
|
|
816
|
+
export async function bootstrapConversationSession(body = {}) {
|
|
817
|
+
if (!areNetworkRequestsEnabled()) return null;
|
|
818
|
+
const response = await fetch(`${BASE}/api/conversation/bootstrap`, {
|
|
819
|
+
signal: requestTimeoutSignal(LONG_REQUEST_TIMEOUT_MS),
|
|
820
|
+
method: 'POST',
|
|
821
|
+
headers: {
|
|
822
|
+
'Content-Type': 'application/json',
|
|
823
|
+
...authHeaders(),
|
|
824
|
+
},
|
|
825
|
+
body: JSON.stringify(body || {}),
|
|
826
|
+
});
|
|
827
|
+
const payload = await response.json().catch(() => null);
|
|
828
|
+
if (!response.ok) {
|
|
829
|
+
const message = String(payload?.error || '').trim();
|
|
830
|
+
const error = new Error(message || `Could not start a new conversation session (HTTP ${response.status}).`);
|
|
831
|
+
// The conversation may already be committed (worker prestart failed after
|
|
832
|
+
// it was created); the caller needs the payload to open it instead of
|
|
833
|
+
// leaving an unreachable row behind.
|
|
834
|
+
error.status = response.status;
|
|
835
|
+
error.payload = payload || null;
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
return payload;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export async function cancelConversationTurn(conversationId, body = {}) {
|
|
842
|
+
const convId = String(conversationId || '').trim();
|
|
843
|
+
if (!convId) return null;
|
|
844
|
+
return apiFetch(`/api/conversation/${encodeURIComponent(convId)}/cancel-turn`, {
|
|
845
|
+
method: 'POST',
|
|
846
|
+
body: JSON.stringify(body || {}),
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export async function cancelQueuedConversationTurn(conversationId, body = {}) {
|
|
851
|
+
const convId = String(conversationId || '').trim();
|
|
852
|
+
if (!convId) return null;
|
|
853
|
+
return apiFetch(`/api/conversation/${encodeURIComponent(convId)}/cancel-queued-turn`, {
|
|
854
|
+
method: 'POST',
|
|
855
|
+
body: JSON.stringify(body || {}),
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function cancelSubagentRun(conversationId, subagentRunId, body = {}) {
|
|
860
|
+
const convId = String(conversationId || '').trim();
|
|
861
|
+
const runId = String(subagentRunId || '').trim();
|
|
862
|
+
if (!convId || !runId) return null;
|
|
863
|
+
return apiFetch(`/api/conversation/${encodeURIComponent(convId)}/subagent/${encodeURIComponent(runId)}/cancel`, {
|
|
864
|
+
method: 'POST',
|
|
865
|
+
body: JSON.stringify(body || {}),
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export async function loadRelayQuestions(status = 'pending') {
|
|
870
|
+
return apiFetch(`/api/relay-questions?status=${encodeURIComponent(status)}`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
export async function loadRelayQuestion(questionId) {
|
|
874
|
+
const id = String(questionId || '').trim();
|
|
875
|
+
if (!id) return null;
|
|
876
|
+
return apiFetch(`/api/relay-question/${encodeURIComponent(id)}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export async function answerRelayQuestion(questionId, answer, sdkSessionId = null) {
|
|
880
|
+
const id = String(questionId || '').trim();
|
|
881
|
+
if (!id) return null;
|
|
882
|
+
const sessionId = String(sdkSessionId || '').trim();
|
|
883
|
+
return apiFetch(`/api/relay-question/${encodeURIComponent(id)}/answer`, {
|
|
884
|
+
method: 'POST',
|
|
885
|
+
body: JSON.stringify({ answer, sdk_session_id: sessionId || undefined }),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export async function answerRelayQuestionStructured(questionId, structuredAnswer, sdkSessionId = null) {
|
|
890
|
+
const id = String(questionId || '').trim();
|
|
891
|
+
if (!id) return { ok: false, error: 'Missing question id' };
|
|
892
|
+
const sessionId = String(sdkSessionId || '').trim();
|
|
893
|
+
try {
|
|
894
|
+
const response = await fetch(`${BASE}/api/relay-question/${encodeURIComponent(id)}/answer`, {
|
|
895
|
+
signal: requestTimeoutSignal(),
|
|
896
|
+
method: 'POST',
|
|
897
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
898
|
+
body: JSON.stringify({ structuredAnswer, sdk_session_id: sessionId || undefined }),
|
|
899
|
+
});
|
|
900
|
+
const data = await response.json().catch(() => null);
|
|
901
|
+
if (!response.ok) {
|
|
902
|
+
return { ok: false, error: data?.error || `Request failed (${response.status})`, fields: data?.fields || null };
|
|
903
|
+
}
|
|
904
|
+
return { ok: true, question: data?.question || null };
|
|
905
|
+
} catch (error) {
|
|
906
|
+
return { ok: false, error: error?.message || 'Network error' };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export async function loadRelayBoards(status = 'pending') {
|
|
911
|
+
return apiFetch(`/api/relay-boards?status=${encodeURIComponent(status)}`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export async function loadRelayBoard(boardId) {
|
|
915
|
+
const id = String(boardId || '').trim();
|
|
916
|
+
if (!id) return null;
|
|
917
|
+
return apiFetch(`/api/relay-board/${encodeURIComponent(id)}`);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
export async function submitRelayBoardAction(boardId, actionId) {
|
|
921
|
+
const id = String(boardId || '').trim();
|
|
922
|
+
const action = String(actionId || '').trim();
|
|
923
|
+
if (!id || !action) return null;
|
|
924
|
+
return apiFetch(`/api/relay-board/${encodeURIComponent(id)}/action`, {
|
|
925
|
+
method: 'POST',
|
|
926
|
+
body: JSON.stringify({ actionId: action }),
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export async function uploadAttachment(item) {
|
|
931
|
+
if (!item?.file) return null;
|
|
932
|
+
const response = await fetch(`${BASE}/api/upload`, {
|
|
933
|
+
signal: requestTimeoutSignal(LONG_REQUEST_TIMEOUT_MS),
|
|
934
|
+
method: 'POST',
|
|
935
|
+
headers: {
|
|
936
|
+
...authHeaders(),
|
|
937
|
+
'X-File-Name': encodeURIComponent(String(item.name || item.file.name || 'upload')),
|
|
938
|
+
'X-File-Type': String(item.type || item.file.type || 'application/octet-stream'),
|
|
939
|
+
},
|
|
940
|
+
body: item.file,
|
|
941
|
+
});
|
|
942
|
+
if (!response.ok) {
|
|
943
|
+
const text = await response.text().catch(() => '');
|
|
944
|
+
throw new Error(text || `Upload failed (${response.status})`);
|
|
945
|
+
}
|
|
946
|
+
return response.json();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export async function loadRepoTree(includeHidden = false, includeHeavy = false, conversationId = null) {
|
|
950
|
+
const params = new URLSearchParams({
|
|
951
|
+
includeHidden: includeHidden ? '1' : '0',
|
|
952
|
+
includeHeavy: includeHeavy ? '1' : '0',
|
|
953
|
+
});
|
|
954
|
+
const convId = String(conversationId || '').trim();
|
|
955
|
+
if (convId) params.set('conversationId', convId);
|
|
956
|
+
return apiFetch(`/api/repo/tree?${params.toString()}`);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export async function loadRepoChildren(pathValue = '', includeHidden = false, includeHeavy = false, conversationId = null) {
|
|
960
|
+
const params = new URLSearchParams({
|
|
961
|
+
path: String(pathValue || '').trim(),
|
|
962
|
+
includeHidden: includeHidden ? '1' : '0',
|
|
963
|
+
includeHeavy: includeHeavy ? '1' : '0',
|
|
964
|
+
});
|
|
965
|
+
const convId = String(conversationId || '').trim();
|
|
966
|
+
if (convId) params.set('conversationId', convId);
|
|
967
|
+
return apiFetch(`/api/repo/list?${params.toString()}`);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export async function loadDrivesRoots() {
|
|
971
|
+
return apiFetch('/api/drives/roots');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export async function loadDriveChildren(pathValue, includeHidden = false) {
|
|
975
|
+
const path = String(pathValue || '').trim();
|
|
976
|
+
if (!path) return null;
|
|
977
|
+
return apiFetch(`/api/drives/list?path=${encodeURIComponent(path)}&includeHidden=${includeHidden ? '1' : '0'}`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Distinct from loadDriveChildren: the session root may not exist on disk yet,
|
|
981
|
+
// and its listing is augmented with the transcript that lives one level up.
|
|
982
|
+
export async function loadSessionRootTree(pathValue, includeHidden = false) {
|
|
983
|
+
const path = String(pathValue || '').trim();
|
|
984
|
+
if (!path) return null;
|
|
985
|
+
return apiFetch(`/api/session-root/list?path=${encodeURIComponent(path)}&includeHidden=${includeHidden ? '1' : '0'}`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export async function loadWorkspaceFilePreview(pathValue, conversationId = null) {
|
|
989
|
+
const path = String(pathValue || '').trim();
|
|
990
|
+
if (!path) return null;
|
|
991
|
+
const encodedPath = path.split('/').map((segment) => encodeURIComponent(segment)).join('/');
|
|
992
|
+
const convId = String(conversationId || '').trim();
|
|
993
|
+
const suffix = convId ? `?conversationId=${encodeURIComponent(convId)}` : '';
|
|
994
|
+
return apiFetch(`/api/files-preview/${encodedPath}${suffix}`);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export async function loadGitStatus(conversationId = null) {
|
|
998
|
+
const params = new URLSearchParams();
|
|
999
|
+
const convId = String(conversationId || '').trim();
|
|
1000
|
+
if (convId) params.set('conversationId', convId);
|
|
1001
|
+
const suffix = params.toString();
|
|
1002
|
+
return apiFetch(`/api/git/status${suffix ? `?${suffix}` : ''}`);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
export async function loadGitDiff(pathValue, { conversationId = null, untracked = false } = {}) {
|
|
1006
|
+
const path = String(pathValue || '').trim();
|
|
1007
|
+
if (!path) return null;
|
|
1008
|
+
const params = new URLSearchParams();
|
|
1009
|
+
params.set('path', path);
|
|
1010
|
+
if (untracked) params.set('untracked', '1');
|
|
1011
|
+
const convId = String(conversationId || '').trim();
|
|
1012
|
+
if (convId) params.set('conversationId', convId);
|
|
1013
|
+
return apiFetch(`/api/git/diff?${params.toString()}`);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export async function requestGitPull(conversationId = null) {
|
|
1017
|
+
const params = new URLSearchParams();
|
|
1018
|
+
const convId = String(conversationId || '').trim();
|
|
1019
|
+
if (convId) params.set('conversationId', convId);
|
|
1020
|
+
const suffix = params.toString();
|
|
1021
|
+
const response = await fetch(`${BASE}/api/git/pull${suffix ? `?${suffix}` : ''}`, {
|
|
1022
|
+
signal: requestTimeoutSignal(LONG_REQUEST_TIMEOUT_MS),
|
|
1023
|
+
method: 'POST',
|
|
1024
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
1025
|
+
}).catch(() => null);
|
|
1026
|
+
if (!response) return { ok: false, error: 'Network error' };
|
|
1027
|
+
const payload = await response.json().catch(() => null);
|
|
1028
|
+
if (!response.ok) return { ok: false, error: payload?.error || `Pull failed (${response.status})` };
|
|
1029
|
+
return payload || { ok: false, error: 'Empty response' };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
export async function loadDriveFilePreview(pathValue) {
|
|
1033
|
+
const path = String(pathValue || '').trim();
|
|
1034
|
+
if (!path) return null;
|
|
1035
|
+
return apiFetch(`/api/drives/files-preview?path=${encodeURIComponent(path)}`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
let contextUsageRefreshTimer = null;
|
|
1039
|
+
let contextUsageRefreshSeq = 0;
|
|
1040
|
+
|
|
1041
|
+
export function scheduleContextUsageRefresh(conversationId, delayMs = 0) {
|
|
1042
|
+
if (contextUsageRefreshTimer) {
|
|
1043
|
+
clearTimeout(contextUsageRefreshTimer);
|
|
1044
|
+
contextUsageRefreshTimer = null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const convId = String(conversationId || '').trim();
|
|
1048
|
+
if (!convId) {
|
|
1049
|
+
contextUsageRefreshSeq += 1;
|
|
1050
|
+
applyContextUsageBar(null);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const seq = ++contextUsageRefreshSeq;
|
|
1055
|
+
const waitMs = Math.max(0, Number(delayMs) || 0);
|
|
1056
|
+
contextUsageRefreshTimer = setTimeout(() => {
|
|
1057
|
+
contextUsageRefreshTimer = null;
|
|
1058
|
+
void refreshContextUsageBar(convId, seq);
|
|
1059
|
+
}, waitMs);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export async function refreshContextUsageBar(conversationId, requestSeq = ++contextUsageRefreshSeq) {
|
|
1063
|
+
const convId = String(conversationId || '').trim();
|
|
1064
|
+
if (!convId) {
|
|
1065
|
+
applyContextUsageBar(null);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const lookupId = resolveContextLookupId(convId) || convId;
|
|
1069
|
+
// Callers fire this without awaiting, so a rejection here would surface as an
|
|
1070
|
+
// unhandled rejection. A miss is routine (a conversation with no turns yet):
|
|
1071
|
+
// leave the bar as-is rather than blanking it.
|
|
1072
|
+
let payload = null;
|
|
1073
|
+
try {
|
|
1074
|
+
payload = await apiFetch(`/api/context/${encodeURIComponent(lookupId)}`);
|
|
1075
|
+
} catch {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (!payload) return;
|
|
1079
|
+
if (requestSeq !== contextUsageRefreshSeq) return;
|
|
1080
|
+
if (String(currentConvId || '').trim() !== convId) return;
|
|
1081
|
+
applyContextUsageBar(readContextUsageRatio(payload));
|
|
1082
|
+
}
|