Connect nyx to assay backend end-to-end

- ws.ts: auth via query params (token + session), store session_id
  from assay session_info, mark connected on ready signal
- useAgentSocket.ts: handlers for session_info, controls, artifacts, cleared
- auth.ts: auto-set dev service token for instant login
- Dockerfile + nginx.conf for K3s deployment
- .env.production: wss://assay.loop42.de/ws

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nico 2026-03-31 21:16:28 +02:00
parent 026c01a8b4
commit a0fee6c121
29 changed files with 41475 additions and 32 deletions

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

21
nginx.conf Normal file
View File

@ -0,0 +1,21 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location /health {
return 200 '{"status":"ok"}';
add_header Content-Type application/json;
}
# SPA fallback all routes serve index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

5602
node_modules/.vite/deps/@heroicons_vue_20_solid.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6552
node_modules/.vite/deps/@heroicons_vue_24_outline.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

67
node_modules/.vite/deps/_metadata.json generated vendored Normal file
View File

@ -0,0 +1,67 @@
{
"hash": "e17e77c0",
"configHash": "dc6d70cb",
"lockfileHash": "6af6fd6d",
"browserHash": "66168846",
"optimized": {
"@heroicons/vue/20/solid": {
"src": "../../@heroicons/vue/20/solid/esm/index.js",
"file": "@heroicons_vue_20_solid.js",
"fileHash": "0066ecc4",
"needsInterop": false
},
"@heroicons/vue/24/outline": {
"src": "../../@heroicons/vue/24/outline/esm/index.js",
"file": "@heroicons_vue_24_outline.js",
"fileHash": "a36c55de",
"needsInterop": false
},
"marked": {
"src": "../../marked/lib/marked.esm.js",
"file": "marked.js",
"fileHash": "b6630fbf",
"needsInterop": false
},
"overlayscrollbars": {
"src": "../../overlayscrollbars/overlayscrollbars.mjs",
"file": "overlayscrollbars.js",
"fileHash": "db5e6c28",
"needsInterop": false
},
"overlayscrollbars-vue": {
"src": "../../overlayscrollbars-vue/overlayscrollbars-vue.mjs",
"file": "overlayscrollbars-vue.js",
"fileHash": "9fa8796d",
"needsInterop": false
},
"pinia": {
"src": "../../pinia/dist/pinia.mjs",
"file": "pinia.js",
"fileHash": "7796a0ac",
"needsInterop": false
},
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "d6de8853",
"needsInterop": false
},
"vue-router": {
"src": "../../vue-router/dist/vue-router.js",
"file": "vue-router.js",
"fileHash": "c9ca5415",
"needsInterop": false
}
},
"chunks": {
"chunk-BAESQNSA": {
"file": "chunk-BAESQNSA.js"
},
"chunk-WX6LVJOK": {
"file": "chunk-WX6LVJOK.js"
},
"chunk-Y2QQXEZR": {
"file": "chunk-Y2QQXEZR.js"
}
}
}

2817
node_modules/.vite/deps/chunk-BAESQNSA.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
node_modules/.vite/deps/chunk-BAESQNSA.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

213
node_modules/.vite/deps/chunk-WX6LVJOK.js generated vendored Normal file
View File

@ -0,0 +1,213 @@
// node_modules/hookable/dist/index.mjs
function flatHooks(configHooks, hooks = {}, parentName) {
for (const key in configHooks) {
const subHook = configHooks[key];
const name = parentName ? `${parentName}:${key}` : key;
if (typeof subHook === "object" && subHook !== null) {
flatHooks(subHook, hooks, name);
} else if (typeof subHook === "function") {
hooks[name] = subHook;
}
}
return hooks;
}
var defaultTask = { run: (function_) => function_() };
var _createTask = () => defaultTask;
var createTask = typeof console.createTask !== "undefined" ? console.createTask : _createTask;
function serialTaskCaller(hooks, args) {
const name = args.shift();
const task = createTask(name);
return hooks.reduce(
(promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))),
Promise.resolve()
);
}
function parallelTaskCaller(hooks, args) {
const name = args.shift();
const task = createTask(name);
return Promise.all(hooks.map((hook) => task.run(() => hook(...args))));
}
function callEachWith(callbacks, arg0) {
for (const callback of [...callbacks]) {
callback(arg0);
}
}
var Hookable = class {
constructor() {
this._hooks = {};
this._before = void 0;
this._after = void 0;
this._deprecatedMessages = void 0;
this._deprecatedHooks = {};
this.hook = this.hook.bind(this);
this.callHook = this.callHook.bind(this);
this.callHookWith = this.callHookWith.bind(this);
}
hook(name, function_, options = {}) {
if (!name || typeof function_ !== "function") {
return () => {
};
}
const originalName = name;
let dep;
while (this._deprecatedHooks[name]) {
dep = this._deprecatedHooks[name];
name = dep.to;
}
if (dep && !options.allowDeprecated) {
let message = dep.message;
if (!message) {
message = `${originalName} hook has been deprecated` + (dep.to ? `, please use ${dep.to}` : "");
}
if (!this._deprecatedMessages) {
this._deprecatedMessages = /* @__PURE__ */ new Set();
}
if (!this._deprecatedMessages.has(message)) {
console.warn(message);
this._deprecatedMessages.add(message);
}
}
if (!function_.name) {
try {
Object.defineProperty(function_, "name", {
get: () => "_" + name.replace(/\W+/g, "_") + "_hook_cb",
configurable: true
});
} catch {
}
}
this._hooks[name] = this._hooks[name] || [];
this._hooks[name].push(function_);
return () => {
if (function_) {
this.removeHook(name, function_);
function_ = void 0;
}
};
}
hookOnce(name, function_) {
let _unreg;
let _function = (...arguments_) => {
if (typeof _unreg === "function") {
_unreg();
}
_unreg = void 0;
_function = void 0;
return function_(...arguments_);
};
_unreg = this.hook(name, _function);
return _unreg;
}
removeHook(name, function_) {
if (this._hooks[name]) {
const index = this._hooks[name].indexOf(function_);
if (index !== -1) {
this._hooks[name].splice(index, 1);
}
if (this._hooks[name].length === 0) {
delete this._hooks[name];
}
}
}
deprecateHook(name, deprecated) {
this._deprecatedHooks[name] = typeof deprecated === "string" ? { to: deprecated } : deprecated;
const _hooks = this._hooks[name] || [];
delete this._hooks[name];
for (const hook of _hooks) {
this.hook(name, hook);
}
}
deprecateHooks(deprecatedHooks) {
Object.assign(this._deprecatedHooks, deprecatedHooks);
for (const name in deprecatedHooks) {
this.deprecateHook(name, deprecatedHooks[name]);
}
}
addHooks(configHooks) {
const hooks = flatHooks(configHooks);
const removeFns = Object.keys(hooks).map(
(key) => this.hook(key, hooks[key])
);
return () => {
for (const unreg of removeFns.splice(0, removeFns.length)) {
unreg();
}
};
}
removeHooks(configHooks) {
const hooks = flatHooks(configHooks);
for (const key in hooks) {
this.removeHook(key, hooks[key]);
}
}
removeAllHooks() {
for (const key in this._hooks) {
delete this._hooks[key];
}
}
callHook(name, ...arguments_) {
arguments_.unshift(name);
return this.callHookWith(serialTaskCaller, name, ...arguments_);
}
callHookParallel(name, ...arguments_) {
arguments_.unshift(name);
return this.callHookWith(parallelTaskCaller, name, ...arguments_);
}
callHookWith(caller, name, ...arguments_) {
const event = this._before || this._after ? { name, args: arguments_, context: {} } : void 0;
if (this._before) {
callEachWith(this._before, event);
}
const result = caller(
name in this._hooks ? [...this._hooks[name]] : [],
arguments_
);
if (result instanceof Promise) {
return result.finally(() => {
if (this._after && event) {
callEachWith(this._after, event);
}
});
}
if (this._after && event) {
callEachWith(this._after, event);
}
return result;
}
beforeEach(function_) {
this._before = this._before || [];
this._before.push(function_);
return () => {
if (this._before !== void 0) {
const index = this._before.indexOf(function_);
if (index !== -1) {
this._before.splice(index, 1);
}
}
};
}
afterEach(function_) {
this._after = this._after || [];
this._after.push(function_);
return () => {
if (this._after !== void 0) {
const index = this._after.indexOf(function_);
if (index !== -1) {
this._after.splice(index, 1);
}
}
};
}
};
function createHooks() {
return new Hookable();
}
// node_modules/birpc/dist/index.mjs
var { clearTimeout, setTimeout } = globalThis;
var random = Math.random.bind(Math);
export {
createHooks
};
//# sourceMappingURL=chunk-WX6LVJOK.js.map

7
node_modules/.vite/deps/chunk-WX6LVJOK.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

12840
node_modules/.vite/deps/chunk-Y2QQXEZR.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
node_modules/.vite/deps/chunk-Y2QQXEZR.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

1226
node_modules/.vite/deps/marked.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
node_modules/.vite/deps/marked.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

170
node_modules/.vite/deps/overlayscrollbars-vue.js generated vendored Normal file
View File

@ -0,0 +1,170 @@
import {
OverlayScrollbars
} from "./chunk-BAESQNSA.js";
import {
createBlock,
createElementBlock,
defineComponent,
onUnmounted,
openBlock,
ref,
renderSlot,
resolveDynamicComponent,
shallowRef,
toRefs,
unref,
watch,
watchPostEffect,
withCtx
} from "./chunk-Y2QQXEZR.js";
// node_modules/overlayscrollbars-vue/overlayscrollbars-vue.mjs
var g = () => {
if (typeof window > "u") {
const o = () => {
};
return [o, o];
}
let a, t;
const n = window, l = typeof n.requestIdleCallback == "function", s = n.requestAnimationFrame, r = n.cancelAnimationFrame, f = l ? n.requestIdleCallback : s, p = l ? n.cancelIdleCallback : r, e = () => {
p(a), r(t);
};
return [
(o, m) => {
e(), a = f(
l ? () => {
e(), t = s(o);
} : o,
typeof m == "object" ? m : { timeout: 2233 }
);
},
e
];
};
var x = (a) => {
let t = null, n, l, s;
const r = shallowRef(a || {}), [f, p] = g();
return watch(
() => {
var e;
return unref((e = r.value) == null ? void 0 : e.defer);
},
(e) => {
s = e;
},
{ deep: true, immediate: true }
), watch(
() => {
var e;
return unref((e = r.value) == null ? void 0 : e.options);
},
(e) => {
n = e, OverlayScrollbars.valid(t) && t.options(n || {}, true);
},
{ deep: true, immediate: true }
), watch(
() => {
var e;
return unref((e = r.value) == null ? void 0 : e.events);
},
(e) => {
l = e, OverlayScrollbars.valid(t) && t.on(
/* c8 ignore next */
l || {},
true
);
},
{ deep: true, immediate: true }
), onUnmounted(() => {
p(), t == null || t.destroy();
}), [
(e) => {
if (OverlayScrollbars.valid(t))
return t;
const o = () => t = OverlayScrollbars(e, n || {}, l || {});
s ? f(o, s) : o();
},
() => t
];
};
var P = defineComponent({
__name: "OverlayScrollbarsComponent",
props: {
element: {
type: [String, Object],
default: "div"
},
options: { type: Object },
events: { type: Object },
defer: { type: [Boolean, Object] }
},
emits: ["osInitialized", "osUpdated", "osDestroyed", "osScroll"],
setup(a, { expose: t, emit: n }) {
const l = a, s = {
initialized: "osInitialized",
updated: "osUpdated",
destroyed: "osDestroyed",
scroll: "osScroll"
}, { element: r, options: f, events: p, defer: e } = toRefs(l), o = shallowRef(null), m = shallowRef(null), I = ref(), [E, O] = x({ options: f, events: I, defer: e });
return t({
osInstance: O,
getElement: () => o.value
}), watchPostEffect((c) => {
const { value: i } = o, { value: v } = m;
i && (E(
r.value === "body" ? {
target: i,
cancel: {
body: null
}
} : {
target: i,
elements: {
viewport: v,
content: v
}
}
), c(() => {
var d;
return (d = O()) == null ? void 0 : d.destroy();
}));
}), watch(
() => unref(p),
(c) => {
const i = c || {};
I.value = Object.keys(s).reduce((v, d) => {
const k = i[d];
return v[d] = [
(...R) => n(
s[d],
...R
),
...(Array.isArray(k) ? k : [k]).filter(Boolean)
], v;
}, {});
},
{ deep: true, immediate: true }
), (c, i) => (openBlock(), createBlock(resolveDynamicComponent(unref(r)), {
"data-overlayscrollbars-initialize": "",
ref_key: "elementRef",
ref: o
}, {
default: withCtx(() => [
unref(r) === "body" ? renderSlot(c.$slots, "default", { key: 0 }) : (openBlock(), createElementBlock("div", {
key: 1,
"data-overlayscrollbars-contents": "",
ref_key: "slotRef",
ref: m
}, [
renderSlot(c.$slots, "default")
], 512))
]),
_: 3
}, 512));
}
});
export {
P as OverlayScrollbarsComponent,
x as useOverlayScrollbars
};
//# sourceMappingURL=overlayscrollbars-vue.js.map

7
node_modules/.vite/deps/overlayscrollbars-vue.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

12
node_modules/.vite/deps/overlayscrollbars.js generated vendored Normal file
View File

@ -0,0 +1,12 @@
import {
$t,
Ht,
OverlayScrollbars,
zt
} from "./chunk-BAESQNSA.js";
export {
zt as ClickScrollPlugin,
OverlayScrollbars,
Ht as ScrollbarsHidingPlugin,
$t as SizeObserverPlugin
};

7
node_modules/.vite/deps/overlayscrollbars.js.map generated vendored Normal file
View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

3
node_modules/.vite/deps/package.json generated vendored Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

5678
node_modules/.vite/deps/pinia.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
node_modules/.vite/deps/pinia.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

5778
node_modules/.vite/deps/vue-router.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
node_modules/.vite/deps/vue-router.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

346
node_modules/.vite/deps/vue.js generated vendored Normal file
View File

@ -0,0 +1,346 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-Y2QQXEZR.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};

7
node_modules/.vite/deps/vue.js.map generated vendored Normal file
View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@ -5,7 +5,14 @@ const SESSION_TOKEN_KEY = 'nyx_session';
import { getApiBase } from '../utils/apiBase';
// Dev service token — auto-login for development
const DEV_TOKEN = '7Oorb9S3OpwFyWgm4zi_Tq7GeamefbjjTgooPVPWAwPDOf6B4TvgvQlLbhmT4DjsqBS_D1g';
export function useAuth(connectFn: () => void) {
// Auto-set dev token if no token exists
if (!localStorage.getItem(SESSION_TOKEN_KEY) && !localStorage.getItem('titan_token')) {
localStorage.setItem(SESSION_TOKEN_KEY, DEV_TOKEN);
}
const isLoggedIn: Ref<boolean> = ref(!!localStorage.getItem(SESSION_TOKEN_KEY));
const loginToken: Ref<string> = ref('');
const loginError: Ref<string> = ref('');

View File

@ -44,7 +44,28 @@ export function useAgentSocket(
auth_ok(data) { updateFromServer(data); },
ready(data) {
updateFromServer(data);
if (data.sessionId) chatStore.sessionKey = data.sessionId;
if (data.session_id) chatStore.sessionKey = data.session_id;
else if (data.sessionId) chatStore.sessionKey = data.sessionId;
},
// assay session info — store session ID for reconnect
session_info(data) {
if (data.session_id) chatStore.sessionKey = data.session_id;
},
// assay UI controls — store for later rendering
controls(_data) {
// TODO: render controls in workspace panel
},
// assay artifacts — store for later rendering
artifacts(_data) {
// TODO: render artifacts in workspace panel
},
// assay session cleared
cleared(_data) {
chatStore.messages.splice(0);
},
thinking(data) {

View File

@ -63,13 +63,27 @@ function getTakeover() {
return _takeover;
}
// Dev service token — skip login UI for now
const DEV_TOKEN = '7Oorb9S3OpwFyWgm4zi_Tq7GeamefbjjTgooPVPWAwPDOf6B4TvgvQlLbhmT4DjsqBS_D1g';
function getWsUrl(): string {
if (import.meta.env.VITE_WS_URL) return import.meta.env.VITE_WS_URL as string;
const params = new URLSearchParams(window.location.search);
const wsHost = params.get('ws') || window.location.hostname;
const wsPort = params.get('port') || window.location.port;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${wsHost}:${wsPort}/ws`;
let base: string;
if (import.meta.env.VITE_WS_URL) {
base = import.meta.env.VITE_WS_URL as string;
} else {
const params = new URLSearchParams(window.location.search);
const wsHost = params.get('ws') || window.location.hostname;
const wsPort = params.get('port') || window.location.port;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
base = `${protocol}//${wsHost}:${wsPort}/ws`;
}
// Append token + session as query params (assay auth pattern)
const token = localStorage.getItem('nyx_session') || localStorage.getItem('titan_token') || DEV_TOKEN;
const session = sessionStorage.getItem('nyx_ws_session') || '';
const qs = new URLSearchParams();
if (token) qs.set('token', token);
if (session) qs.set('session', session);
return qs.toString() ? `${base}?${qs}` : base;
}
function scheduleReconnect() {
@ -108,16 +122,8 @@ function connect(
_ws.onopen = () => {
_reconnectDelay = 1000;
const agent = selectedAgent.value;
const token = localStorage.getItem('nyx_session') || localStorage.getItem('titan_token');
const mode = _selectedModeRef?.value ?? 'private';
// Send auth even without agent — BE returns ready with agent list
_ws?.send(JSON.stringify(
token
? { type: 'auth', agent: agent || '', token, mode }
: { type: 'connect', agent: agent || '', user: 'nico', mode }
));
getTakeover().reregister();
// Auth is via query params — no auth message needed
// Just start ping keepalive
if (_pingInterval) clearInterval(_pingInterval);
_pingInterval = setInterval(() => {
if (_ws?.readyState === WebSocket.OPEN) _ws.send(JSON.stringify({ type: 'ping' }));
@ -132,11 +138,18 @@ function connect(
getTakeover().dispatch(data.cmdId, data.cmd, data.args || {}, send);
return;
}
// Session info from assay — store for reconnect
if (data.type === 'session_info') {
sessionId.value = data.session_id;
sessionStorage.setItem('nyx_ws_session', data.session_id);
}
// Ready signal from assay — mark connected
if (data.type === 'ready') {
connected.value = true;
status.value = 'Connected';
}
if (data.type === 'error' && data.code === 'SESSION_TERMINATED') {
console.warn('Message bounced: Session terminated.');
} else if (data.type === 'diagnostic') {
const logMethod = (console as any)[data.level] || console.log;
logMethod(`Backend Diagnostic (${data.level.toUpperCase()}):`, data.message);
}
_messageBuffer.push(data);
_onMessageCallbacks.forEach(fn => fn(data));
@ -184,18 +197,7 @@ function disconnect(): void {
}
function sendDeferredAuth(): void {
if (!_pendingAuth || !_ws || _ws.readyState !== WebSocket.OPEN) return;
const agent = _selectedAgentRef?.value;
if (!agent) return;
_pendingAuth = false;
const token = localStorage.getItem('nyx_session') || localStorage.getItem('titan_token');
const mode = _selectedModeRef?.value ?? 'private';
_ws.send(JSON.stringify(
token
? { type: 'auth', agent, token, mode }
: { type: 'connect', agent, user: 'nico', mode }
));
getTakeover().reregister();
// Auth is via query params now — no-op for backward compat
}
// Poll for agent becoming available after deferred auth (no Vue watch to avoid circular imports)