Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05f193df7b | ||
|
|
c9b5bb1b7a | ||
|
|
ba1013cd35 | ||
|
|
ec6428702b | ||
|
|
e08ebb2057 | ||
|
|
9683f90f7e | ||
|
|
06cb986aa6 | ||
|
|
a85c2f1700 |
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
"google-auth-library": "^10.5.0"
|
"google-auth-library": "^10.5.0"
|
||||||
@@ -7389,7 +7389,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
"@neuralnomads/codenomad": "file:../server"
|
"@neuralnomads/codenomad": "file:../server"
|
||||||
@@ -7423,7 +7423,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
@@ -7458,14 +7458,14 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Neural Nomads",
|
"name": "Neural Nomads",
|
||||||
|
|||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Neural Nomads",
|
"name": "Neural Nomads",
|
||||||
|
|||||||
@@ -127,10 +127,18 @@ function parsePort(input: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveHost(input: string | undefined): string {
|
function resolveHost(input: string | undefined): string {
|
||||||
if (input && input.trim() === "0.0.0.0") {
|
const trimmed = input?.trim()
|
||||||
|
if (!trimmed) return DEFAULT_HOST
|
||||||
|
|
||||||
|
if (trimmed === "0.0.0.0") {
|
||||||
return "0.0.0.0"
|
return "0.0.0.0"
|
||||||
}
|
}
|
||||||
return DEFAULT_HOST
|
|
||||||
|
if (trimmed === "localhost") {
|
||||||
|
return DEFAULT_HOST
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -149,11 +157,13 @@ async function main() {
|
|||||||
|
|
||||||
const eventBus = new EventBus(eventLogger)
|
const eventBus = new EventBus(eventLogger)
|
||||||
|
|
||||||
|
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||||
|
|
||||||
const serverMeta: ServerMeta = {
|
const serverMeta: ServerMeta = {
|
||||||
httpBaseUrl: `http://${options.host}:${options.port}`,
|
httpBaseUrl: `http://${options.host}:${options.port}`,
|
||||||
eventsUrl: `/api/events`,
|
eventsUrl: `/api/events`,
|
||||||
host: options.host,
|
host: options.host,
|
||||||
listeningMode: options.host === "0.0.0.0" ? "all" : "local",
|
listeningMode: isLoopbackHost(options.host) ? "local" : "all",
|
||||||
port: options.port,
|
port: options.port,
|
||||||
hostLabel: options.host,
|
hostLabel: options.host,
|
||||||
workspaceRoot: options.rootDir,
|
workspaceRoot: options.rootDir,
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const allowedDevOrigins = new Set(["http://localhost:3000", "http://127.0.0.1:3000"])
|
const allowedDevOrigins = new Set(["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||||
|
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||||
|
|
||||||
app.register(cors, {
|
app.register(cors, {
|
||||||
origin: (origin, cb) => {
|
origin: (origin, cb) => {
|
||||||
@@ -113,10 +114,17 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowedDevOrigins.has(origin)) {
|
if (allowedDevOrigins.has(origin)) {
|
||||||
cb(null, true)
|
cb(null, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When we bind to a non-loopback host (e.g., 0.0.0.0 or LAN IP), allow cross-origin UI access.
|
||||||
|
if (deps.host === "0.0.0.0" || !isLoopbackHost(deps.host)) {
|
||||||
|
cb(null, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
cb(null, false)
|
cb(null, false)
|
||||||
},
|
},
|
||||||
@@ -275,13 +283,13 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayHost = deps.host === "0.0.0.0" ? "127.0.0.1" : deps.host === "127.0.0.1" ? "localhost" : deps.host
|
const displayHost = deps.host === "127.0.0.1" ? "localhost" : deps.host
|
||||||
const serverUrl = `http://${displayHost}:${actualPort}`
|
const serverUrl = `http://${displayHost}:${actualPort}`
|
||||||
|
|
||||||
deps.serverMeta.httpBaseUrl = serverUrl
|
deps.serverMeta.httpBaseUrl = serverUrl
|
||||||
deps.serverMeta.host = deps.host
|
deps.serverMeta.host = deps.host
|
||||||
deps.serverMeta.port = actualPort
|
deps.serverMeta.port = actualPort
|
||||||
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" ? "all" : "local"
|
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" || !isLoopbackHost(deps.host) ? "all" : "local"
|
||||||
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening")
|
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening")
|
||||||
console.log(`CodeNomad Server is ready at ${serverUrl}`)
|
console.log(`CodeNomad Server is ready at ${serverUrl}`)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function buildMetaResponse(meta: ServerMeta): ServerMeta {
|
|||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
port,
|
port,
|
||||||
listeningMode: meta.host === "0.0.0.0" ? "all" : "local",
|
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
|
||||||
addresses,
|
addresses,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,10 @@ function resolvePort(meta: ServerMeta): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLoopbackHost(host: string): boolean {
|
||||||
|
return host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAddresses(port: number, host: string): NetworkAddress[] {
|
function resolveAddresses(port: number, host: string): NetworkAddress[] {
|
||||||
const interfaces = os.networkInterfaces()
|
const interfaces = os.networkInterfaces()
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.7.2",
|
"version": "0.7.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setExpandState("normal")
|
||||||
clearPrompt()
|
clearPrompt()
|
||||||
|
|
||||||
// Ignore attachments for slash commands, but keep them for next prompt.
|
// Ignore attachments for slash commands, but keep them for next prompt.
|
||||||
@@ -843,7 +844,10 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const currentPrompt = prompt()
|
const currentPrompt = prompt()
|
||||||
const pos = atPosition()
|
const pos = atPosition()
|
||||||
const cursorPos = textareaRef?.selectionStart || 0
|
const cursorPos = textareaRef?.selectionStart || 0
|
||||||
const folderMention = relativePath === "." || relativePath === "" ? "/" : displayPath
|
const folderMention =
|
||||||
|
relativePath === "." || relativePath === ""
|
||||||
|
? "/"
|
||||||
|
: relativePath.replace(/\/+$/, "") + "/"
|
||||||
|
|
||||||
if (pos !== null) {
|
if (pos !== null) {
|
||||||
const before = currentPrompt.substring(0, pos + 1)
|
const before = currentPrompt.substring(0, pos + 1)
|
||||||
@@ -887,7 +891,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
if (pos !== null) {
|
if (pos !== null) {
|
||||||
const before = currentPrompt.substring(0, pos)
|
const before = currentPrompt.substring(0, pos)
|
||||||
const after = currentPrompt.substring(cursorPos)
|
const after = currentPrompt.substring(cursorPos)
|
||||||
const attachmentText = `@${filename}`
|
const attachmentText = `@${normalizedPath}`
|
||||||
const newPrompt = before + attachmentText + " " + after
|
const newPrompt = before + attachmentText + " " + after
|
||||||
setPrompt(newPrompt)
|
setPrompt(newPrompt)
|
||||||
|
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
||||||
scrollToSelected()
|
scrollToSelected()
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter" || e.key === "Tab") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const selected = items[selectedIndex()]
|
const selected = items[selectedIndex()]
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -534,7 +534,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
|
|||||||
|
|
||||||
<div class="dropdown-footer">
|
<div class="dropdown-footer">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">↑↓</span> navigate • <span class="font-medium">Enter</span> select •{" "}
|
<span class="font-medium">↑↓</span> navigate • <span class="font-medium">Tab/Enter</span> select •{" "}
|
||||||
<span class="font-medium">Esc</span> close
|
<span class="font-medium">Esc</span> close
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,6 +92,19 @@ function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor): Instanc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureActiveInstanceSelected(): void {
|
||||||
|
const current = activeInstanceId()
|
||||||
|
const instanceMap = instances()
|
||||||
|
if (current && instanceMap.has(current)) return
|
||||||
|
|
||||||
|
for (const [id, instance] of instanceMap.entries()) {
|
||||||
|
if (instance.status === "ready") {
|
||||||
|
setActiveInstanceId(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
||||||
const mapped = workspaceDescriptorToInstance(descriptor)
|
const mapped = workspaceDescriptorToInstance(descriptor)
|
||||||
if (instances().has(descriptor.id)) {
|
if (instances().has(descriptor.id)) {
|
||||||
@@ -102,6 +115,9 @@ function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
|||||||
|
|
||||||
if (descriptor.status === "ready") {
|
if (descriptor.status === "ready") {
|
||||||
attachClient(descriptor)
|
attachClient(descriptor)
|
||||||
|
// If no tab is currently selected (common after UI refresh),
|
||||||
|
// auto-select the first ready instance.
|
||||||
|
ensureActiveInstanceSelected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,15 +241,18 @@ async function hydrateInstanceData(instanceId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async function initializeWorkspaces() {
|
void (async function initializeWorkspaces() {
|
||||||
try {
|
try {
|
||||||
const workspaces = await serverApi.fetchWorkspaces()
|
const workspaces = await serverApi.fetchWorkspaces()
|
||||||
workspaces.forEach((workspace) => upsertWorkspace(workspace))
|
workspaces.forEach((workspace) => upsertWorkspace(workspace))
|
||||||
|
// After a UI refresh, we may have instances but no active selection.
|
||||||
|
ensureActiveInstanceSelected()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to load workspaces", error)
|
log.error("Failed to load workspaces", error)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
||||||
serverEvents.on("*", (event) => handleWorkspaceEvent(event))
|
serverEvents.on("*", (event) => handleWorkspaceEvent(event))
|
||||||
|
|
||||||
function handleWorkspaceEvent(event: WorkspaceEventPayload) {
|
function handleWorkspaceEvent(event: WorkspaceEventPayload) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { loadMessages } from "./session-api"
|
|||||||
import {
|
import {
|
||||||
applyPartUpdateV2,
|
applyPartUpdateV2,
|
||||||
replaceMessageIdV2,
|
replaceMessageIdV2,
|
||||||
|
reconcilePendingQuestionsV2,
|
||||||
upsertMessageInfoV2,
|
upsertMessageInfoV2,
|
||||||
upsertPermissionV2,
|
upsertPermissionV2,
|
||||||
upsertQuestionV2,
|
upsertQuestionV2,
|
||||||
@@ -230,6 +231,10 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
|
|
||||||
applyPartUpdateV2(instanceId, { ...part, sessionID: sessionId, messageID: messageId })
|
applyPartUpdateV2(instanceId, { ...part, sessionID: sessionId, messageID: messageId })
|
||||||
|
|
||||||
|
if (part.type === "tool" && part.tool === "question") {
|
||||||
|
// Questions can arrive before their tool part exists; re-link now.
|
||||||
|
reconcilePendingQuestionsV2(instanceId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
} else if (event.type === "message.updated") {
|
} else if (event.type === "message.updated") {
|
||||||
|
|||||||
Reference in New Issue
Block a user