@cryptiklemur/lattice 0.0.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/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useStore } from "@tanstack/react-store";
|
|
3
|
+
import type { ProjectInfo } from "@lattice/shared";
|
|
4
|
+
import type { ProjectsListMessage } from "@lattice/shared";
|
|
5
|
+
import type { ServerMessage } from "@lattice/shared";
|
|
6
|
+
import { useWebSocket } from "./useWebSocket";
|
|
7
|
+
import { setActiveProjectSlug, getSidebarStore } from "../stores/sidebar";
|
|
8
|
+
|
|
9
|
+
export interface UseProjectsResult {
|
|
10
|
+
projects: ProjectInfo[];
|
|
11
|
+
activeProject: ProjectInfo | null;
|
|
12
|
+
setActiveProject: (project: ProjectInfo | null) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useProjects(): UseProjectsResult {
|
|
16
|
+
var ws = useWebSocket();
|
|
17
|
+
var [projects, setProjects] = useState<ProjectInfo[]>([]);
|
|
18
|
+
var activeProjectSlug = useStore(getSidebarStore(), function (state) { return state.activeProjectSlug; });
|
|
19
|
+
|
|
20
|
+
var handleRef = useRef<(msg: ServerMessage) => void>(function () {});
|
|
21
|
+
|
|
22
|
+
useEffect(function () {
|
|
23
|
+
handleRef.current = function (msg: ServerMessage) {
|
|
24
|
+
if (msg.type === "projects:list") {
|
|
25
|
+
var list = (msg as ProjectsListMessage).projects;
|
|
26
|
+
setProjects(list);
|
|
27
|
+
var storeState = getSidebarStore().state;
|
|
28
|
+
var currentSlug = storeState.activeProjectSlug;
|
|
29
|
+
if (currentSlug !== null) {
|
|
30
|
+
var found = list.find(function (p) { return p.slug === currentSlug; });
|
|
31
|
+
if (!found && list.length > 0) {
|
|
32
|
+
setActiveProjectSlug(list[0].slug);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
useEffect(function () {
|
|
40
|
+
function handler(msg: ServerMessage) {
|
|
41
|
+
handleRef.current(msg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ws.subscribe("projects:list", handler);
|
|
45
|
+
|
|
46
|
+
return function () {
|
|
47
|
+
ws.unsubscribe("projects:list", handler);
|
|
48
|
+
};
|
|
49
|
+
}, [ws]);
|
|
50
|
+
|
|
51
|
+
useEffect(function () {
|
|
52
|
+
if (ws.status === "connected") {
|
|
53
|
+
ws.send({ type: "settings:get" });
|
|
54
|
+
}
|
|
55
|
+
}, [ws.status, ws]);
|
|
56
|
+
|
|
57
|
+
var activeProject = activeProjectSlug !== null
|
|
58
|
+
? (projects.find(function (p) { return p.slug === activeProjectSlug; }) ?? null)
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
function setActiveProject(project: ProjectInfo | null): void {
|
|
62
|
+
setActiveProjectSlug(project ? project.slug : null);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { projects, activeProject, setActiveProject };
|
|
66
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export type SaveState = "idle" | "saved" | "error";
|
|
4
|
+
|
|
5
|
+
export interface UseSaveStateReturn {
|
|
6
|
+
dirty: boolean;
|
|
7
|
+
saving: boolean;
|
|
8
|
+
saveState: SaveState;
|
|
9
|
+
markDirty: () => void;
|
|
10
|
+
startSave: () => void;
|
|
11
|
+
confirmSave: () => void;
|
|
12
|
+
resetFromServer: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useSaveState(): UseSaveStateReturn {
|
|
16
|
+
var [dirty, setDirty] = useState(false);
|
|
17
|
+
var [saving, setSaving] = useState(false);
|
|
18
|
+
var [saveState, setSaveState] = useState<SaveState>("idle");
|
|
19
|
+
var saveTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(function () {
|
|
22
|
+
return function () {
|
|
23
|
+
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
function markDirty() {
|
|
28
|
+
setDirty(true);
|
|
29
|
+
setSaveState("idle");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function startSave() {
|
|
33
|
+
setSaving(true);
|
|
34
|
+
setSaveState("idle");
|
|
35
|
+
|
|
36
|
+
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
37
|
+
saveTimeout.current = setTimeout(function () {
|
|
38
|
+
setSaving(false);
|
|
39
|
+
setSaveState("error");
|
|
40
|
+
setTimeout(function () { setSaveState("idle"); }, 3000);
|
|
41
|
+
}, 5000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function confirmSave() {
|
|
45
|
+
setSaving(false);
|
|
46
|
+
setSaveState("saved");
|
|
47
|
+
setDirty(false);
|
|
48
|
+
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
49
|
+
saveTimeout.current = setTimeout(function () { setSaveState("idle"); }, 1800);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resetFromServer() {
|
|
53
|
+
setDirty(false);
|
|
54
|
+
setSaveState("idle");
|
|
55
|
+
setSaving(false);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { dirty, saving, saveState, markDirty, startSave, confirmSave, resetFromServer };
|
|
59
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useStore } from "@tanstack/react-store";
|
|
3
|
+
import type { HistoryMessage } from "@lattice/shared";
|
|
4
|
+
import type {
|
|
5
|
+
ChatDeltaMessage,
|
|
6
|
+
ChatSendMessage,
|
|
7
|
+
ChatToolStartMessage,
|
|
8
|
+
ChatToolResultMessage,
|
|
9
|
+
ChatPermissionRequestMessage,
|
|
10
|
+
ChatPermissionResolvedMessage,
|
|
11
|
+
ChatUserMessage,
|
|
12
|
+
ChatStatusMessage,
|
|
13
|
+
ChatContextUsageMessage,
|
|
14
|
+
ChatContextBreakdownMessage,
|
|
15
|
+
SessionHistoryMessage,
|
|
16
|
+
ServerMessage,
|
|
17
|
+
} from "@lattice/shared";
|
|
18
|
+
import { useWebSocket } from "./useWebSocket";
|
|
19
|
+
import { setActiveSessionId as setSidebarSessionId } from "../stores/sidebar";
|
|
20
|
+
import {
|
|
21
|
+
getSessionStore,
|
|
22
|
+
setSessionMessages,
|
|
23
|
+
addSessionMessage,
|
|
24
|
+
updateLastAssistantMessage,
|
|
25
|
+
updateToolResult,
|
|
26
|
+
setIsProcessing,
|
|
27
|
+
setActiveSession,
|
|
28
|
+
setSessionTitle,
|
|
29
|
+
setCurrentStatus,
|
|
30
|
+
setContextUsage,
|
|
31
|
+
setContextBreakdown,
|
|
32
|
+
getCurrentAssistantUuid,
|
|
33
|
+
setCurrentAssistantUuid,
|
|
34
|
+
incrementPendingPermissions,
|
|
35
|
+
updatePermissionStatus,
|
|
36
|
+
setLastResponseStats,
|
|
37
|
+
getLastReadIndex,
|
|
38
|
+
setLastReadIndex,
|
|
39
|
+
markSessionRead,
|
|
40
|
+
getStreamGeneration,
|
|
41
|
+
mergeToolResults,
|
|
42
|
+
setWasInterrupted,
|
|
43
|
+
} from "../stores/session";
|
|
44
|
+
import type { SessionState } from "../stores/session";
|
|
45
|
+
|
|
46
|
+
var subscriptionsActive = 0;
|
|
47
|
+
var activeStreamGeneration = 0;
|
|
48
|
+
|
|
49
|
+
export type { SessionState };
|
|
50
|
+
|
|
51
|
+
export interface UseSessionReturn extends SessionState {
|
|
52
|
+
sendMessage: (text: string, model?: string, effort?: string) => void;
|
|
53
|
+
activateSession: (projectSlug: string, sessionId: string) => void;
|
|
54
|
+
lastReadIndex: number | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useSession(): UseSessionReturn {
|
|
58
|
+
var store = getSessionStore();
|
|
59
|
+
var state = useStore(store, function (s) { return s; });
|
|
60
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
61
|
+
var sendRef = useRef(send);
|
|
62
|
+
sendRef.current = send;
|
|
63
|
+
|
|
64
|
+
function activateSession(projectSlug: string, sessionId: string) {
|
|
65
|
+
setActiveSession(projectSlug, sessionId);
|
|
66
|
+
setSidebarSessionId(sessionId);
|
|
67
|
+
sendRef.current({ type: "session:activate", projectSlug, sessionId });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sendMessage(text: string, model?: string, effort?: string) {
|
|
71
|
+
var currentSessionId = getSessionStore().state.activeSessionId;
|
|
72
|
+
if (!currentSessionId || !text.trim()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
var msg = { type: "chat:send" as const, text: text } as ChatSendMessage & { model?: string; effort?: string };
|
|
76
|
+
if (model && model !== "default") {
|
|
77
|
+
msg.model = model;
|
|
78
|
+
}
|
|
79
|
+
if (effort) {
|
|
80
|
+
msg.effort = effort;
|
|
81
|
+
}
|
|
82
|
+
activeStreamGeneration = getStreamGeneration();
|
|
83
|
+
sendRef.current(msg as ChatSendMessage);
|
|
84
|
+
setIsProcessing(true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
useEffect(function () {
|
|
88
|
+
subscriptionsActive++;
|
|
89
|
+
if (subscriptionsActive > 1) {
|
|
90
|
+
return function () { subscriptionsActive--; };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isStaleStream(): boolean {
|
|
94
|
+
return activeStreamGeneration !== getStreamGeneration();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleUserMessage(msg: ServerMessage) {
|
|
98
|
+
if (isStaleStream()) return;
|
|
99
|
+
var m = msg as ChatUserMessage;
|
|
100
|
+
setCurrentAssistantUuid(null);
|
|
101
|
+
addSessionMessage({
|
|
102
|
+
type: "user",
|
|
103
|
+
uuid: m.uuid,
|
|
104
|
+
text: m.text,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
} as HistoryMessage);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleDelta(msg: ServerMessage) {
|
|
110
|
+
if (isStaleStream()) return;
|
|
111
|
+
var m = msg as ChatDeltaMessage;
|
|
112
|
+
var uuid = getCurrentAssistantUuid();
|
|
113
|
+
|
|
114
|
+
if (!uuid) {
|
|
115
|
+
var newUuid = "assistant-" + Date.now();
|
|
116
|
+
setCurrentAssistantUuid(newUuid);
|
|
117
|
+
addSessionMessage({
|
|
118
|
+
type: "assistant",
|
|
119
|
+
uuid: newUuid,
|
|
120
|
+
text: m.text,
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
} as HistoryMessage);
|
|
123
|
+
} else {
|
|
124
|
+
updateLastAssistantMessage(uuid, m.text);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleToolStart(msg: ServerMessage) {
|
|
129
|
+
if (isStaleStream()) return;
|
|
130
|
+
var m = msg as ChatToolStartMessage;
|
|
131
|
+
setCurrentAssistantUuid(null);
|
|
132
|
+
addSessionMessage({
|
|
133
|
+
type: "tool_start",
|
|
134
|
+
toolId: m.toolId,
|
|
135
|
+
name: m.name,
|
|
136
|
+
args: m.args,
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
} as HistoryMessage);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleToolResult(msg: ServerMessage) {
|
|
142
|
+
if (isStaleStream()) return;
|
|
143
|
+
var m = msg as ChatToolResultMessage;
|
|
144
|
+
updateToolResult(m.toolId, m.content);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleDone(msg: ServerMessage) {
|
|
148
|
+
if (isStaleStream()) return;
|
|
149
|
+
var m = msg as { type: string; cost: number; duration: number; sessionId?: string };
|
|
150
|
+
setIsProcessing(false);
|
|
151
|
+
setCurrentStatus(null);
|
|
152
|
+
setCurrentAssistantUuid(null);
|
|
153
|
+
setLastResponseStats(m.cost || 0, m.duration || 0);
|
|
154
|
+
var activeId = getSessionStore().state.activeSessionId;
|
|
155
|
+
if (activeId) {
|
|
156
|
+
markSessionRead(activeId, getSessionStore().state.messages.length);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handleError(msg: ServerMessage) {
|
|
161
|
+
if (isStaleStream()) return;
|
|
162
|
+
var m = msg as { type: string; message?: string };
|
|
163
|
+
setIsProcessing(false);
|
|
164
|
+
setCurrentStatus(null);
|
|
165
|
+
setCurrentAssistantUuid(null);
|
|
166
|
+
if (m.message) {
|
|
167
|
+
addSessionMessage({
|
|
168
|
+
type: "assistant",
|
|
169
|
+
uuid: "error-" + Date.now(),
|
|
170
|
+
text: "Error: " + m.message,
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
} as HistoryMessage);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleStatus(msg: ServerMessage) {
|
|
177
|
+
if (isStaleStream()) return;
|
|
178
|
+
var m = msg as ChatStatusMessage;
|
|
179
|
+
setCurrentStatus({ phase: m.phase, toolName: m.toolName, elapsed: m.elapsed, summary: m.summary });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleContextUsage(msg: ServerMessage) {
|
|
183
|
+
var m = msg as ChatContextUsageMessage;
|
|
184
|
+
setContextUsage({
|
|
185
|
+
inputTokens: m.inputTokens,
|
|
186
|
+
outputTokens: m.outputTokens,
|
|
187
|
+
cacheReadTokens: m.cacheReadTokens,
|
|
188
|
+
cacheCreationTokens: m.cacheCreationTokens,
|
|
189
|
+
contextWindow: m.contextWindow,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handleContextBreakdown(msg: ServerMessage) {
|
|
194
|
+
var m = msg as ChatContextBreakdownMessage;
|
|
195
|
+
setContextBreakdown({
|
|
196
|
+
segments: m.segments,
|
|
197
|
+
contextWindow: m.contextWindow,
|
|
198
|
+
autocompactAt: m.autocompactAt,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handlePermissionRequest(msg: ServerMessage) {
|
|
203
|
+
var m = msg as ChatPermissionRequestMessage;
|
|
204
|
+
setCurrentAssistantUuid(null);
|
|
205
|
+
addSessionMessage({
|
|
206
|
+
type: "permission_request",
|
|
207
|
+
toolId: m.requestId,
|
|
208
|
+
name: m.tool,
|
|
209
|
+
args: m.args,
|
|
210
|
+
title: m.title,
|
|
211
|
+
decisionReason: m.decisionReason,
|
|
212
|
+
permissionRule: m.permissionRule,
|
|
213
|
+
permissionStatus: "pending",
|
|
214
|
+
timestamp: Date.now(),
|
|
215
|
+
} as HistoryMessage);
|
|
216
|
+
incrementPendingPermissions();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handlePermissionResolved(msg: ServerMessage) {
|
|
220
|
+
var m = msg as ChatPermissionResolvedMessage;
|
|
221
|
+
updatePermissionStatus(m.requestId, m.status);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleHistory(msg: ServerMessage) {
|
|
225
|
+
var m = msg as SessionHistoryMessage;
|
|
226
|
+
setCurrentAssistantUuid(null);
|
|
227
|
+
if (m.sessionId) {
|
|
228
|
+
var projectSlug = m.projectSlug || getSessionStore().state.activeProjectSlug;
|
|
229
|
+
setSidebarSessionId(m.sessionId);
|
|
230
|
+
activeStreamGeneration = getStreamGeneration();
|
|
231
|
+
getSessionStore().setState(function (state) {
|
|
232
|
+
return {
|
|
233
|
+
...state,
|
|
234
|
+
activeProjectSlug: projectSlug,
|
|
235
|
+
activeSessionId: m.sessionId,
|
|
236
|
+
activeSessionTitle: m.title ?? null,
|
|
237
|
+
messages: mergeToolResults(m.messages),
|
|
238
|
+
isProcessing: false,
|
|
239
|
+
currentStatus: null,
|
|
240
|
+
pendingPermissionCount: 0,
|
|
241
|
+
lastResponseCost: null,
|
|
242
|
+
lastResponseDuration: null,
|
|
243
|
+
lastReadIndex: null,
|
|
244
|
+
historyLoading: false,
|
|
245
|
+
wasInterrupted: m.interrupted || false,
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
var storedIndex = getLastReadIndex(m.sessionId);
|
|
249
|
+
if (storedIndex >= 0 && storedIndex < m.messages.length) {
|
|
250
|
+
setLastReadIndex(storedIndex);
|
|
251
|
+
}
|
|
252
|
+
markSessionRead(m.sessionId, m.messages.length);
|
|
253
|
+
} else {
|
|
254
|
+
setSessionMessages(m.messages);
|
|
255
|
+
setIsProcessing(false);
|
|
256
|
+
setCurrentStatus(null);
|
|
257
|
+
if (m.projectSlug) {
|
|
258
|
+
getSessionStore().setState(function (state) {
|
|
259
|
+
return { ...state, activeProjectSlug: m.projectSlug };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (m.title) {
|
|
263
|
+
setSessionTitle(m.title);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
setSessionMessages(m.messages);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
subscribe("chat:user_message", handleUserMessage);
|
|
270
|
+
subscribe("chat:delta", handleDelta);
|
|
271
|
+
subscribe("chat:tool_start", handleToolStart);
|
|
272
|
+
subscribe("chat:tool_result", handleToolResult);
|
|
273
|
+
subscribe("chat:done", handleDone);
|
|
274
|
+
subscribe("chat:error", handleError);
|
|
275
|
+
subscribe("chat:permission_request", handlePermissionRequest);
|
|
276
|
+
subscribe("chat:permission_resolved", handlePermissionResolved);
|
|
277
|
+
subscribe("chat:status", handleStatus);
|
|
278
|
+
subscribe("chat:context_usage", handleContextUsage);
|
|
279
|
+
subscribe("chat:context_breakdown", handleContextBreakdown);
|
|
280
|
+
subscribe("session:history", handleHistory);
|
|
281
|
+
|
|
282
|
+
return function () {
|
|
283
|
+
subscriptionsActive--;
|
|
284
|
+
unsubscribe("chat:user_message", handleUserMessage);
|
|
285
|
+
unsubscribe("chat:delta", handleDelta);
|
|
286
|
+
unsubscribe("chat:tool_start", handleToolStart);
|
|
287
|
+
unsubscribe("chat:tool_result", handleToolResult);
|
|
288
|
+
unsubscribe("chat:done", handleDone);
|
|
289
|
+
unsubscribe("chat:error", handleError);
|
|
290
|
+
unsubscribe("chat:permission_request", handlePermissionRequest);
|
|
291
|
+
unsubscribe("chat:permission_resolved", handlePermissionResolved);
|
|
292
|
+
unsubscribe("chat:status", handleStatus);
|
|
293
|
+
unsubscribe("chat:context_usage", handleContextUsage);
|
|
294
|
+
unsubscribe("chat:context_breakdown", handleContextBreakdown);
|
|
295
|
+
unsubscribe("session:history", handleHistory);
|
|
296
|
+
};
|
|
297
|
+
}, [subscribe, unsubscribe]);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
messages: state.messages,
|
|
301
|
+
isProcessing: state.isProcessing,
|
|
302
|
+
activeProjectSlug: state.activeProjectSlug,
|
|
303
|
+
activeSessionId: state.activeSessionId,
|
|
304
|
+
activeSessionTitle: state.activeSessionTitle,
|
|
305
|
+
sendMessage,
|
|
306
|
+
activateSession,
|
|
307
|
+
currentStatus: state.currentStatus,
|
|
308
|
+
contextUsage: state.contextUsage,
|
|
309
|
+
contextBreakdown: state.contextBreakdown,
|
|
310
|
+
pendingPermissionCount: state.pendingPermissionCount,
|
|
311
|
+
lastResponseCost: state.lastResponseCost,
|
|
312
|
+
lastResponseDuration: state.lastResponseDuration,
|
|
313
|
+
lastReadIndex: state.lastReadIndex,
|
|
314
|
+
historyLoading: state.historyLoading,
|
|
315
|
+
wasInterrupted: state.wasInterrupted,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useStore } from "@tanstack/react-store";
|
|
2
|
+
import {
|
|
3
|
+
getSidebarStore,
|
|
4
|
+
setActiveProjectSlug,
|
|
5
|
+
setActiveSessionId,
|
|
6
|
+
openSettings,
|
|
7
|
+
setSettingsSection,
|
|
8
|
+
openProjectSettings,
|
|
9
|
+
setProjectSettingsSection,
|
|
10
|
+
exitSettings,
|
|
11
|
+
toggleUserMenu,
|
|
12
|
+
toggleProjectDropdown,
|
|
13
|
+
closeMenus,
|
|
14
|
+
goToProjectDashboard,
|
|
15
|
+
goToDashboard,
|
|
16
|
+
toggleDrawer,
|
|
17
|
+
closeDrawer,
|
|
18
|
+
openNodeSettings,
|
|
19
|
+
closeNodeSettings,
|
|
20
|
+
navigateToSession,
|
|
21
|
+
} from "../stores/sidebar";
|
|
22
|
+
import type { SidebarState, SettingsSection, ProjectSettingsSection } from "../stores/sidebar";
|
|
23
|
+
|
|
24
|
+
export function useSidebar(): SidebarState & {
|
|
25
|
+
setActiveProjectSlug: (slug: string | null) => void;
|
|
26
|
+
setActiveSessionId: (sessionId: string | null) => void;
|
|
27
|
+
openSettings: (section: SettingsSection) => void;
|
|
28
|
+
setSettingsSection: (section: SettingsSection) => void;
|
|
29
|
+
openProjectSettings: (section: ProjectSettingsSection) => void;
|
|
30
|
+
setProjectSettingsSection: (section: ProjectSettingsSection) => void;
|
|
31
|
+
exitSettings: () => void;
|
|
32
|
+
toggleUserMenu: () => void;
|
|
33
|
+
toggleProjectDropdown: () => void;
|
|
34
|
+
closeMenus: () => void;
|
|
35
|
+
goToProjectDashboard: () => void;
|
|
36
|
+
goToDashboard: () => void;
|
|
37
|
+
toggleDrawer: () => void;
|
|
38
|
+
closeDrawer: () => void;
|
|
39
|
+
openNodeSettings: () => void;
|
|
40
|
+
closeNodeSettings: () => void;
|
|
41
|
+
navigateToSession: (projectSlug: string, sessionId: string) => void;
|
|
42
|
+
} {
|
|
43
|
+
var store = getSidebarStore();
|
|
44
|
+
var state = useStore(store, function (s) { return s; });
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
activeProjectSlug: state.activeProjectSlug,
|
|
48
|
+
activeSessionId: state.activeSessionId,
|
|
49
|
+
sidebarMode: state.sidebarMode,
|
|
50
|
+
activeView: state.activeView,
|
|
51
|
+
previousView: state.previousView,
|
|
52
|
+
userMenuOpen: state.userMenuOpen,
|
|
53
|
+
projectDropdownOpen: state.projectDropdownOpen,
|
|
54
|
+
drawerOpen: state.drawerOpen,
|
|
55
|
+
setActiveProjectSlug: setActiveProjectSlug,
|
|
56
|
+
setActiveSessionId: setActiveSessionId,
|
|
57
|
+
openSettings: openSettings,
|
|
58
|
+
setSettingsSection: setSettingsSection,
|
|
59
|
+
openProjectSettings: openProjectSettings,
|
|
60
|
+
setProjectSettingsSection: setProjectSettingsSection,
|
|
61
|
+
exitSettings: exitSettings,
|
|
62
|
+
toggleUserMenu: toggleUserMenu,
|
|
63
|
+
toggleProjectDropdown: toggleProjectDropdown,
|
|
64
|
+
closeMenus: closeMenus,
|
|
65
|
+
goToProjectDashboard: goToProjectDashboard,
|
|
66
|
+
goToDashboard: goToDashboard,
|
|
67
|
+
toggleDrawer: toggleDrawer,
|
|
68
|
+
closeDrawer: closeDrawer,
|
|
69
|
+
navigateToSession: navigateToSession,
|
|
70
|
+
openNodeSettings: openNodeSettings,
|
|
71
|
+
closeNodeSettings: closeNodeSettings,
|
|
72
|
+
nodeSettingsOpen: state.nodeSettingsOpen,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { SkillInfo } from "@lattice/shared";
|
|
3
|
+
import type { ServerMessage } from "@lattice/shared";
|
|
4
|
+
import { useWebSocket } from "./useWebSocket";
|
|
5
|
+
|
|
6
|
+
export function useSkills(): SkillInfo[] {
|
|
7
|
+
var ws = useWebSocket();
|
|
8
|
+
var [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
9
|
+
|
|
10
|
+
useEffect(function () {
|
|
11
|
+
function handleSkillsList(msg: ServerMessage) {
|
|
12
|
+
if (msg.type === "skills:list") {
|
|
13
|
+
var listMsg = msg as { type: string; skills: SkillInfo[] };
|
|
14
|
+
setSkills(listMsg.skills);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
ws.subscribe("skills:list", handleSkillsList);
|
|
18
|
+
return function () {
|
|
19
|
+
ws.unsubscribe("skills:list", handleSkillsList);
|
|
20
|
+
};
|
|
21
|
+
}, [ws]);
|
|
22
|
+
|
|
23
|
+
useEffect(function () {
|
|
24
|
+
if (ws.status === "connected") {
|
|
25
|
+
ws.send({ type: "skills:list_request" });
|
|
26
|
+
}
|
|
27
|
+
}, [ws.status, ws]);
|
|
28
|
+
|
|
29
|
+
return skills;
|
|
30
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useStore } from "@tanstack/react-store";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { getThemeStore, toggleMode, setThemeForMode } from "../stores/theme";
|
|
4
|
+
import { themes, type ThemeEntry } from "../themes/index";
|
|
5
|
+
|
|
6
|
+
function hexToOklch(hex: string): string {
|
|
7
|
+
var r = parseInt(hex.slice(0, 2), 16) / 255;
|
|
8
|
+
var g = parseInt(hex.slice(2, 4), 16) / 255;
|
|
9
|
+
var b = parseInt(hex.slice(4, 6), 16) / 255;
|
|
10
|
+
|
|
11
|
+
r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
|
|
12
|
+
g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
|
|
13
|
+
b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
|
|
14
|
+
|
|
15
|
+
var x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b;
|
|
16
|
+
var y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;
|
|
17
|
+
var z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b;
|
|
18
|
+
|
|
19
|
+
var l_ = 0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z;
|
|
20
|
+
var m_ = 0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z;
|
|
21
|
+
var s_ = 0.0482003018 * x + 0.2643662691 * y + 0.6338517070 * z;
|
|
22
|
+
|
|
23
|
+
var l3 = Math.cbrt(l_);
|
|
24
|
+
var m3 = Math.cbrt(m_);
|
|
25
|
+
var s3 = Math.cbrt(s_);
|
|
26
|
+
|
|
27
|
+
var L = 0.2104542553 * l3 + 0.7936177850 * m3 - 0.0040720468 * s3;
|
|
28
|
+
var a = 1.9779984951 * l3 - 2.4285922050 * m3 + 0.4505937099 * s3;
|
|
29
|
+
var bOk = 0.0259040371 * l3 + 0.7827717662 * m3 - 0.8086757660 * s3;
|
|
30
|
+
|
|
31
|
+
var C = Math.sqrt(a * a + bOk * bOk);
|
|
32
|
+
var h = Math.atan2(bOk, a) * 180 / Math.PI;
|
|
33
|
+
if (h < 0) h += 360;
|
|
34
|
+
|
|
35
|
+
return (L * 100).toFixed(1) + "% " + C.toFixed(3) + " " + h.toFixed(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function contrastContent(hex: string): string {
|
|
39
|
+
var r = parseInt(hex.slice(0, 2), 16);
|
|
40
|
+
var g = parseInt(hex.slice(2, 4), 16);
|
|
41
|
+
var b = parseInt(hex.slice(4, 6), 16);
|
|
42
|
+
var lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
43
|
+
return lum > 0.5 ? "15% 0.01 0" : "98% 0.01 0";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function applyTheme(entry: ThemeEntry): void {
|
|
47
|
+
var root = document.documentElement;
|
|
48
|
+
var t = entry.theme;
|
|
49
|
+
|
|
50
|
+
root.style.setProperty("--base00", "#" + t.base00);
|
|
51
|
+
root.style.setProperty("--base01", "#" + t.base01);
|
|
52
|
+
root.style.setProperty("--base02", "#" + t.base02);
|
|
53
|
+
root.style.setProperty("--base03", "#" + t.base03);
|
|
54
|
+
root.style.setProperty("--base04", "#" + t.base04);
|
|
55
|
+
root.style.setProperty("--base05", "#" + t.base05);
|
|
56
|
+
root.style.setProperty("--base06", "#" + t.base06);
|
|
57
|
+
root.style.setProperty("--base07", "#" + t.base07);
|
|
58
|
+
root.style.setProperty("--base08", "#" + t.base08);
|
|
59
|
+
root.style.setProperty("--base09", "#" + t.base09);
|
|
60
|
+
root.style.setProperty("--base0A", "#" + t.base0A);
|
|
61
|
+
root.style.setProperty("--base0B", "#" + t.base0B);
|
|
62
|
+
root.style.setProperty("--base0C", "#" + t.base0C);
|
|
63
|
+
root.style.setProperty("--base0D", "#" + t.base0D);
|
|
64
|
+
root.style.setProperty("--base0E", "#" + t.base0E);
|
|
65
|
+
root.style.setProperty("--base0F", "#" + t.base0F);
|
|
66
|
+
|
|
67
|
+
root.style.setProperty("--color-base-100", "oklch(" + hexToOklch(t.base00) + ")");
|
|
68
|
+
root.style.setProperty("--color-base-200", "oklch(" + hexToOklch(t.base01) + ")");
|
|
69
|
+
root.style.setProperty("--color-base-300", "oklch(" + hexToOklch(t.base02) + ")");
|
|
70
|
+
root.style.setProperty("--color-base-content", "oklch(" + hexToOklch(t.base05) + ")");
|
|
71
|
+
root.style.setProperty("--color-primary", "oklch(" + hexToOklch(t.base0D) + ")");
|
|
72
|
+
root.style.setProperty("--color-primary-content", "oklch(" + contrastContent(t.base0D) + ")");
|
|
73
|
+
root.style.setProperty("--color-secondary", "oklch(" + hexToOklch(t.base0E) + ")");
|
|
74
|
+
root.style.setProperty("--color-secondary-content", "oklch(" + contrastContent(t.base0E) + ")");
|
|
75
|
+
root.style.setProperty("--color-accent", "oklch(" + hexToOklch(t.base0C) + ")");
|
|
76
|
+
root.style.setProperty("--color-accent-content", "oklch(" + contrastContent(t.base0C) + ")");
|
|
77
|
+
root.style.setProperty("--color-neutral", "oklch(" + hexToOklch(t.base02) + ")");
|
|
78
|
+
root.style.setProperty("--color-neutral-content", "oklch(" + hexToOklch(t.base04) + ")");
|
|
79
|
+
root.style.setProperty("--color-info", "oklch(" + hexToOklch(t.base0C) + ")");
|
|
80
|
+
root.style.setProperty("--color-info-content", "oklch(" + contrastContent(t.base0C) + ")");
|
|
81
|
+
root.style.setProperty("--color-success", "oklch(" + hexToOklch(t.base0B) + ")");
|
|
82
|
+
root.style.setProperty("--color-success-content", "oklch(" + contrastContent(t.base0B) + ")");
|
|
83
|
+
root.style.setProperty("--color-warning", "oklch(" + hexToOklch(t.base0A) + ")");
|
|
84
|
+
root.style.setProperty("--color-warning-content", "oklch(" + contrastContent(t.base0A) + ")");
|
|
85
|
+
root.style.setProperty("--color-error", "oklch(" + hexToOklch(t.base08) + ")");
|
|
86
|
+
root.style.setProperty("--color-error-content", "oklch(" + contrastContent(t.base08) + ")");
|
|
87
|
+
|
|
88
|
+
root.dataset.theme = entry.theme.variant === "dark" ? "lattice-dark" : "lattice-light";
|
|
89
|
+
root.style.colorScheme = entry.theme.variant === "dark" ? "dark" : "light";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function useTheme() {
|
|
93
|
+
var store = getThemeStore();
|
|
94
|
+
var state = useStore(store, function (s) { return s; });
|
|
95
|
+
|
|
96
|
+
var currentThemeId = state.mode === "dark" ? state.darkThemeId : state.lightThemeId;
|
|
97
|
+
var currentEntry = themes.find(function (e) { return e.id === currentThemeId; }) ?? themes[0];
|
|
98
|
+
|
|
99
|
+
useEffect(function () {
|
|
100
|
+
applyTheme(currentEntry);
|
|
101
|
+
}, [currentEntry]);
|
|
102
|
+
|
|
103
|
+
function setTheme(themeId: string): void {
|
|
104
|
+
setThemeForMode(themeId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
mode: state.mode,
|
|
109
|
+
currentThemeId,
|
|
110
|
+
toggleMode,
|
|
111
|
+
setTheme,
|
|
112
|
+
themes,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { ClientMessage, ServerMessage } from "@lattice/shared";
|
|
3
|
+
|
|
4
|
+
export type WebSocketStatus = "connecting" | "connected" | "disconnected";
|
|
5
|
+
|
|
6
|
+
export interface WebSocketContextValue {
|
|
7
|
+
status: WebSocketStatus;
|
|
8
|
+
send: (msg: ClientMessage) => void;
|
|
9
|
+
subscribe: (type: string, callback: (msg: ServerMessage) => void) => void;
|
|
10
|
+
unsubscribe: (type: string, callback: (msg: ServerMessage) => void) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export var WebSocketContext = createContext<WebSocketContextValue | null>(null);
|
|
14
|
+
|
|
15
|
+
export function useWebSocket(): WebSocketContextValue {
|
|
16
|
+
var ctx = useContext(WebSocketContext);
|
|
17
|
+
if (!ctx) {
|
|
18
|
+
throw new Error("useWebSocket must be used within a WebSocketProvider");
|
|
19
|
+
}
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getWebSocketUrl(): string {
|
|
24
|
+
var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
25
|
+
return protocol + "//" + window.location.host + "/ws";
|
|
26
|
+
}
|