Compare commits

...

3 Commits

Author SHA1 Message Date
Shantur Rathore
69f221942c Bump version to 0.7.1 2026-01-15 08:39:06 +00:00
Shantur Rathore
7749225f71 fix(ui): restore pasted text expand controls\n\nFixes #67 2026-01-15 08:36:56 +00:00
Shantur Rathore
ae322c53cc fix(ui): correct Go to Session navigation across instances 2026-01-15 08:26:49 +00:00
11 changed files with 115 additions and 26 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.7.0", "version": "0.7.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.7.0", "version": "0.7.1",
"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.0", "version": "0.7.1",
"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.0", "version": "0.7.1",
"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.0", "version": "0.7.1",
"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.0", "version": "0.7.1",
"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",

View File

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

View File

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

View File

@@ -3,6 +3,6 @@
"version": "0.5.0", "version": "0.5.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@opencode-ai/plugin": "1.1.12" "@opencode-ai/plugin": "1.1.16"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,8 +82,20 @@ interface TaskSessionLocation {
parentId: string | null parentId: string | null
} }
function findTaskSessionLocation(sessionId: string): TaskSessionLocation | null { function findTaskSessionLocation(sessionId: string, preferredInstanceId?: string): TaskSessionLocation | null {
if (!sessionId) return null if (!sessionId) return null
if (preferredInstanceId) {
const session = sessions().get(preferredInstanceId)?.get(sessionId)
if (session) {
return {
sessionId: session.id,
instanceId: preferredInstanceId,
parentId: session.parentId ?? null,
}
}
}
const allSessions = sessions() const allSessions = sessions()
for (const [instanceId, sessionMap] of allSessions) { for (const [instanceId, sessionMap] of allSessions) {
const session = sessionMap?.get(sessionId) const session = sessionMap?.get(sessionId)
@@ -440,7 +452,7 @@ export default function MessageBlock(props: MessageBlockProps) {
const hasToolState = const hasToolState =
Boolean(toolState) && (isToolStateRunning(toolState) || isToolStateCompleted(toolState) || isToolStateError(toolState)) Boolean(toolState) && (isToolStateRunning(toolState) || isToolStateCompleted(toolState) || isToolStateError(toolState))
const taskSessionId = hasToolState ? extractTaskSessionId(toolState) : "" const taskSessionId = hasToolState ? extractTaskSessionId(toolState) : ""
const taskLocation = taskSessionId ? findTaskSessionLocation(taskSessionId) : null const taskLocation = taskSessionId ? findTaskSessionLocation(taskSessionId, props.instanceId) : null
const handleGoToTaskSession = (event: MouseEvent) => { const handleGoToTaskSession = (event: MouseEvent) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()

View File

@@ -10,7 +10,7 @@ import {
setActivePermissionIdForInstance, setActivePermissionIdForInstance,
setActiveQuestionIdForInstance, setActiveQuestionIdForInstance,
} from "../stores/instances" } from "../stores/instances"
import { loadMessages, setActiveSession } from "../stores/sessions" import { ensureSessionParentExpanded, loadMessages, sessions as sessionStateSessions, setActiveSessionFromList } from "../stores/sessions"
import { messageStoreBus } from "../stores/message-v2/bus" import { messageStoreBus } from "../stores/message-v2/bus"
import ToolCall from "./tool-call" import ToolCall from "./tool-call"
@@ -201,7 +201,14 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
function handleGoToSession(sessionId: string) { function handleGoToSession(sessionId: string) {
if (!sessionId) return if (!sessionId) return
setActiveSession(props.instanceId, sessionId)
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
const parentId = session?.parentId ?? session?.id
if (parentId) {
ensureSessionParentExpanded(props.instanceId, parentId)
}
setActiveSessionFromList(props.instanceId, sessionId)
props.onClose() props.onClose()
} }

View File

@@ -1,11 +1,12 @@
import { Show, For, createMemo, createEffect, type Component } from "solid-js" import { Show, For, createMemo, createEffect, type Component } from "solid-js"
import { Expand } from "lucide-solid"
import type { Session } from "../../types/session" import type { Session } from "../../types/session"
import type { Attachment } from "../../types/attachment" import type { Attachment } from "../../types/attachment"
import type { ClientPart } from "../../types/message" import type { ClientPart } from "../../types/message"
import MessageSection from "../message-section" import MessageSection from "../message-section"
import { messageStoreBus } from "../../stores/message-v2/bus" import { messageStoreBus } from "../../stores/message-v2/bus"
import PromptInput from "../prompt-input" import PromptInput from "../prompt-input"
import AttachmentChip from "../attachment-chip" import type { Attachment as PromptAttachment } from "../../types/attachment"
import { getAttachments, removeAttachment } from "../../stores/attachments" import { getAttachments, removeAttachment } from "../../stores/attachments"
import { instances } from "../../stores/instances" import { instances } from "../../stores/instances"
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions" import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
@@ -49,6 +50,54 @@ export const SessionView: Component<SessionViewProps> = (props) => {
}) })
const attachments = createMemo(() => getAttachments(props.instanceId, props.sessionId)) const attachments = createMemo(() => getAttachments(props.instanceId, props.sessionId))
function handleExpandTextAttachment(attachment: PromptAttachment) {
if (attachment.source.type !== "text") return
const textarea = rootRef?.querySelector(".prompt-input") as HTMLTextAreaElement | null
const value = attachment.source.value
const match = attachment.display.match(/pasted #(\d+)/)
const placeholder = match ? `[pasted #${match[1]}]` : null
const currentText = textarea?.value ?? ""
let nextText = currentText
let selectionTarget: number | null = null
if (placeholder) {
const placeholderIndex = currentText.indexOf(placeholder)
if (placeholderIndex !== -1) {
nextText =
currentText.substring(0, placeholderIndex) +
value +
currentText.substring(placeholderIndex + placeholder.length)
selectionTarget = placeholderIndex + value.length
}
}
if (nextText === currentText) {
if (textarea) {
const start = textarea.selectionStart
const end = textarea.selectionEnd
nextText = currentText.substring(0, start) + value + currentText.substring(end)
selectionTarget = start + value.length
} else {
nextText = currentText + value
}
}
if (textarea) {
textarea.value = nextText
textarea.dispatchEvent(new Event("input", { bubbles: true }))
textarea.focus()
if (selectionTarget !== null) {
textarea.setSelectionRange(selectionTarget, selectionTarget)
}
}
removeAttachment(props.instanceId, props.sessionId, attachment.id)
}
let scrollToBottomHandle: (() => void) | undefined let scrollToBottomHandle: (() => void) | undefined
let rootRef: HTMLDivElement | undefined let rootRef: HTMLDivElement | undefined
function scheduleScrollToBottom() { function scheduleScrollToBottom() {
@@ -235,14 +284,35 @@ export const SessionView: Component<SessionViewProps> = (props) => {
<Show when={attachments().length > 0}> <Show when={attachments().length > 0}>
<div class="flex flex-wrap gap-1.5 border-t px-3 py-2" style="border-color: var(--border-base);"> <div class="flex flex-wrap items-center gap-1.5 border-t px-3 py-2" style="border-color: var(--border-base);">
<For each={attachments()}> <For each={attachments()}>
{(attachment) => ( {(attachment) => {
<AttachmentChip const isText = attachment.source.type === "text"
attachment={attachment} return (
onRemove={() => removeAttachment(props.instanceId, props.sessionId, attachment.id)} <div class="attachment-chip" title={attachment.source.type === "file" ? attachment.source.path : undefined}>
/> <span class="font-mono">{attachment.display}</span>
)} <Show when={isText}>
<button
type="button"
class="attachment-expand"
onClick={() => handleExpandTextAttachment(attachment)}
aria-label="Expand pasted text"
title="Insert pasted text"
>
<Expand class="h-3 w-3" aria-hidden="true" />
</button>
</Show>
<button
type="button"
class="attachment-remove"
onClick={() => removeAttachment(props.instanceId, props.sessionId, attachment.id)}
aria-label="Remove attachment"
>
×
</button>
</div>
)
}}
</For> </For>
</div> </div>
</Show> </Show>