Compare commits

..

8 Commits

Author SHA1 Message Date
Shantur Rathore
05f193df7b fix(ui): auto-select first ready instance after refresh 2026-01-20 19:28:56 +00:00
Shantur Rathore
c9b5bb1b7a Release 0.7.4 2026-01-20 19:20:41 +00:00
Shantur Rathore
ba1013cd35 fix(ui): re-link pending question tool parts (#74) 2026-01-20 19:20:18 +00:00
Shantur Rathore
ec6428702b Bump version to 0.7.3 2026-01-20 18:49:18 +00:00
Shantur Rathore
e08ebb2057 fix(server): honor --host binding
Fixes #75
2026-01-20 18:47:40 +00:00
Shantur Rathore
9683f90f7e fix(ui): insert full paths for @file mentions 2026-01-20 18:47:40 +00:00
Shantur Rathore
06cb986aa6 fix(ui): allow Tab to select from picker
Fixes #77
2026-01-20 18:47:40 +00:00
Shantur Rathore
a85c2f1700 fix(ui): collapse prompt input after send
Fixes #76
2026-01-20 18:47:40 +00:00
14 changed files with 78 additions and 28 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codenomad-workspace",
"version": "0.7.2",
"version": "0.7.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codenomad-workspace",
"version": "0.7.2",
"version": "0.7.4",
"dependencies": {
"7zip-bin": "^5.2.0",
"google-auth-library": "^10.5.0"
@@ -7389,7 +7389,7 @@
},
"packages/electron-app": {
"name": "@neuralnomads/codenomad-electron-app",
"version": "0.7.2",
"version": "0.7.4",
"dependencies": {
"@codenomad/ui": "file:../ui",
"@neuralnomads/codenomad": "file:../server"
@@ -7423,7 +7423,7 @@
},
"packages/server": {
"name": "@neuralnomads/codenomad",
"version": "0.7.2",
"version": "0.7.4",
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0",
@@ -7458,14 +7458,14 @@
},
"packages/tauri-app": {
"name": "@codenomad/tauri-app",
"version": "0.7.2",
"version": "0.7.4",
"devDependencies": {
"@tauri-apps/cli": "^2.9.4"
}
},
"packages/ui": {
"name": "@codenomad/ui",
"version": "0.7.2",
"version": "0.7.4",
"dependencies": {
"@git-diff-view/solid": "^0.0.8",
"@kobalte/core": "0.13.11",

View File

@@ -1,6 +1,6 @@
{
"name": "codenomad-workspace",
"version": "0.7.2",
"version": "0.7.4",
"private": true,
"description": "CodeNomad monorepo workspace",
"workspaces": {

View File

@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad-electron-app",
"version": "0.7.2",
"version": "0.7.4",
"description": "CodeNomad - AI coding assistant",
"author": {
"name": "Neural Nomads",

View File

@@ -1,12 +1,12 @@
{
"name": "@neuralnomads/codenomad",
"version": "0.7.2",
"version": "0.7.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@neuralnomads/codenomad",
"version": "0.7.2",
"version": "0.7.4",
"dependencies": {
"@fastify/cors": "^8.5.0",
"commander": "^12.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad",
"version": "0.7.2",
"version": "0.7.4",
"description": "CodeNomad Server",
"author": {
"name": "Neural Nomads",

View File

@@ -127,10 +127,18 @@ function parsePort(input: string): number {
}
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 DEFAULT_HOST
if (trimmed === "localhost") {
return DEFAULT_HOST
}
return trimmed
}
async function main() {
@@ -149,11 +157,13 @@ async function main() {
const eventBus = new EventBus(eventLogger)
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
const serverMeta: ServerMeta = {
httpBaseUrl: `http://${options.host}:${options.port}`,
eventsUrl: `/api/events`,
host: options.host,
listeningMode: options.host === "0.0.0.0" ? "all" : "local",
listeningMode: isLoopbackHost(options.host) ? "local" : "all",
port: options.port,
hostLabel: options.host,
workspaceRoot: options.rootDir,

View File

@@ -93,6 +93,7 @@ export function createHttpServer(deps: HttpServerDeps) {
})
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, {
origin: (origin, cb) => {
@@ -113,10 +114,17 @@ export function createHttpServer(deps: HttpServerDeps) {
return
}
if (allowedDevOrigins.has(origin)) {
cb(null, true)
return
}
if (allowedDevOrigins.has(origin)) {
cb(null, true)
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)
},
@@ -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}`
deps.serverMeta.httpBaseUrl = serverUrl
deps.serverMeta.host = deps.host
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")
console.log(`CodeNomad Server is ready at ${serverUrl}`)

View File

@@ -17,7 +17,7 @@ function buildMetaResponse(meta: ServerMeta): ServerMeta {
return {
...meta,
port,
listeningMode: meta.host === "0.0.0.0" ? "all" : "local",
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
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[] {
const interfaces = os.networkInterfaces()
const seen = new Set<string>()

View File

@@ -1,6 +1,6 @@
{
"name": "@codenomad/tauri-app",
"version": "0.7.2",
"version": "0.7.4",
"private": true,
"scripts": {
"dev": "tauri dev",

View File

@@ -1,6 +1,6 @@
{
"name": "@codenomad/ui",
"version": "0.7.2",
"version": "0.7.4",
"private": true,
"type": "module",
"scripts": {

View File

@@ -604,6 +604,7 @@ export default function PromptInput(props: PromptInputProps) {
}
}
setExpandState("normal")
clearPrompt()
// 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 pos = atPosition()
const cursorPos = textareaRef?.selectionStart || 0
const folderMention = relativePath === "." || relativePath === "" ? "/" : displayPath
const folderMention =
relativePath === "." || relativePath === ""
? "/"
: relativePath.replace(/\/+$/, "") + "/"
if (pos !== null) {
const before = currentPrompt.substring(0, pos + 1)
@@ -887,7 +891,7 @@ export default function PromptInput(props: PromptInputProps) {
if (pos !== null) {
const before = currentPrompt.substring(0, pos)
const after = currentPrompt.substring(cursorPos)
const attachmentText = `@${filename}`
const attachmentText = `@${normalizedPath}`
const newPrompt = before + attachmentText + " " + after
setPrompt(newPrompt)

View File

@@ -339,7 +339,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
e.preventDefault()
setSelectedIndex((prev) => Math.max(prev - 1, 0))
scrollToSelected()
} else if (e.key === "Enter") {
} else if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault()
const selected = items[selectedIndex()]
if (selected) {
@@ -534,7 +534,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
<div class="dropdown-footer">
<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
</div>
</div>

View File

@@ -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) {
const mapped = workspaceDescriptorToInstance(descriptor)
if (instances().has(descriptor.id)) {
@@ -102,6 +115,9 @@ function upsertWorkspace(descriptor: WorkspaceDescriptor) {
if (descriptor.status === "ready") {
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 {
const workspaces = await serverApi.fetchWorkspaces()
workspaces.forEach((workspace) => upsertWorkspace(workspace))
// After a UI refresh, we may have instances but no active selection.
ensureActiveInstanceSelected()
} catch (error) {
log.error("Failed to load workspaces", error)
}
})()
serverEvents.on("*", (event) => handleWorkspaceEvent(event))
function handleWorkspaceEvent(event: WorkspaceEventPayload) {

View File

@@ -39,6 +39,7 @@ import { loadMessages } from "./session-api"
import {
applyPartUpdateV2,
replaceMessageIdV2,
reconcilePendingQuestionsV2,
upsertMessageInfoV2,
upsertPermissionV2,
upsertQuestionV2,
@@ -230,6 +231,10 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
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)
} else if (event.type === "message.updated") {