From 1af01680eead568b9837414bad8b236898f21df3 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Fri, 30 Jan 2026 17:34:25 +0000 Subject: [PATCH 01/14] feat(ui): add session sidebar search and bulk selection Adds an optional session filter bar to the left sidebar with title search across parent/child sessions and a scoped Select All. Introduces multi-select checkboxes, bulk delete with clear selection controls, and confirmation dialogs for both single and bulk deletions using the existing alert dialog flow. Updates session i18n strings across supported locales. --- .../components/instance/instance-shell2.tsx | 1 + packages/ui/src/components/session-list.tsx | 355 ++++++++++++++++-- .../ui/src/lib/i18n/messages/en/session.ts | 19 + .../ui/src/lib/i18n/messages/es/session.ts | 19 + .../ui/src/lib/i18n/messages/fr/session.ts | 19 + .../ui/src/lib/i18n/messages/ja/session.ts | 19 + .../ui/src/lib/i18n/messages/ru/session.ts | 19 + .../src/lib/i18n/messages/zh-Hans/session.ts | 19 + 8 files changed, 429 insertions(+), 41 deletions(-) diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 308cb81c..cd1e3d66 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -911,6 +911,7 @@ const InstanceShell2: Component = (props) => { void result.catch((error) => log.error("Failed to create session:", error)) } }} + enableFilterBar showHeader={false} showFooter={false} /> diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index 841f50b5..2a8119a9 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -2,12 +2,13 @@ import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCl import type { SessionStatus } from "../types/session" import type { SessionThread } from "../stores/session-state" import { getSessionStatus } from "../stores/session-status" -import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid" +import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare } from "lucide-solid" import KeyboardHint from "./keyboard-hint" import SessionRenameDialog from "./session-rename-dialog" import { keyboardRegistry } from "../lib/keyboard-registry" import { showToastNotification } from "../lib/notifications" import { useI18n } from "../lib/i18n" +import { showConfirmDialog } from "../stores/alerts" import { deleteSession, ensureSessionParentExpanded, @@ -35,6 +36,7 @@ interface SessionListProps { showFooter?: boolean headerContent?: JSX.Element footerContent?: JSX.Element + enableFilterBar?: boolean } function formatSessionStatus(status: SessionStatus): string { @@ -46,6 +48,70 @@ const SessionList: Component = (props) => { const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null) const [isRenaming, setIsRenaming] = createSignal(false) + const [filterQuery, setFilterQuery] = createSignal("") + const normalizedQuery = createMemo(() => filterQuery().trim().toLowerCase()) + + const [selectedSessionIds, setSelectedSessionIds] = createSignal>(new Set()) + + const normalizeSessionLabel = (sessionId: string) => { + const session = sessionStateSessions().get(props.instanceId)?.get(sessionId) + const title = (session?.title ?? "").trim() + return title || t("sessionList.session.untitled") + } + + const sessionMatchesQuery = (sessionId: string, query: string) => { + if (!query) return true + const label = normalizeSessionLabel(sessionId).toLowerCase() + if (label.includes(query)) return true + return sessionId.toLowerCase().includes(query) + } + + const filteredThreads = createMemo(() => { + const query = normalizedQuery() + if (!query) return props.threads + + const next: SessionThread[] = [] + for (const thread of props.threads) { + const parentMatches = sessionMatchesQuery(thread.parent.id, query) + const matchingChildren = thread.children.filter((child) => sessionMatchesQuery(child.id, query)) + + if (!parentMatches && matchingChildren.length === 0) continue + + next.push({ + parent: thread.parent, + children: matchingChildren, + latestUpdated: thread.latestUpdated, + }) + } + + return next + }) + + const allMatchingSessionIds = createMemo(() => { + const ids: string[] = [] + for (const thread of filteredThreads()) { + ids.push(thread.parent.id) + for (const child of thread.children) ids.push(child.id) + } + return ids + }) + + const selectedCount = createMemo(() => selectedSessionIds().size) + + const isAllSelected = createMemo(() => { + const ids = allMatchingSessionIds() + if (ids.length === 0) return false + const selected = selectedSessionIds() + return ids.every((id) => selected.has(id)) + }) + const isSelectAllIndeterminate = createMemo(() => { + const ids = allMatchingSessionIds() + const total = ids.length + if (total === 0) return false + const count = selectedCount() + return count > 0 && count < total + }) + const isSessionDeleting = (sessionId: string) => { const deleting = loading().deletingSession.get(props.instanceId) return deleting ? deleting.has(sessionId) : false @@ -82,6 +148,17 @@ const SessionList: Component = (props) => { event.stopPropagation() if (isSessionDeleting(sessionId)) return + const confirmed = await showConfirmDialog( + t("sessionList.delete.confirmMessage", { label: normalizeSessionLabel(sessionId) }), + { + title: t("sessionList.delete.title"), + variant: "warning", + confirmLabel: t("sessionList.delete.confirmLabel"), + cancelLabel: t("sessionList.delete.cancelLabel"), + }, + ) + if (!confirmed) return + const shouldSelectFallback = props.activeSessionId === sessionId let fallbackSessionId: string | undefined @@ -152,6 +229,115 @@ const SessionList: Component = (props) => { setIsRenaming(false) } } + + const setSelectedMany = (sessionIds: string[], checked: boolean) => { + if (sessionIds.length === 0) return + setSelectedSessionIds((prev) => { + const next = new Set(prev) + sessionIds.forEach((id) => { + if (checked) next.add(id) + else next.delete(id) + }) + return next + }) + } + + const getSelectableThreadIds = (parentId: string): string[] => { + const query = normalizedQuery() + const source = query ? filteredThreads() : props.threads + const thread = source.find((t) => t.parent.id === parentId) + if (!thread) return [parentId] + return [thread.parent.id, ...thread.children.map((c) => c.id)] + } + + const getAllSessionIdsInOrder = (threads: SessionThread[]): string[] => { + const ids: string[] = [] + threads.forEach((thread) => { + ids.push(thread.parent.id) + thread.children.forEach((child) => ids.push(child.id)) + }) + return ids + } + + const handleToggleSelectAll = (checked: boolean) => { + const ids = allMatchingSessionIds() + setSelectedMany(ids, checked) + } + + const toggleSelectAll = () => { + if (isAllSelected()) { + handleToggleSelectAll(false) + return + } + handleToggleSelectAll(true) + } + + const handleBulkDelete = async () => { + const selected = Array.from(selectedSessionIds()) + if (selected.length === 0) return + + const confirmed = await showConfirmDialog( + t("sessionList.bulkDelete.confirmMessage", { count: selected.length }), + { + title: t("sessionList.bulkDelete.title"), + variant: "warning", + confirmLabel: t("sessionList.bulkDelete.confirmLabel"), + cancelLabel: t("sessionList.bulkDelete.cancelLabel"), + }, + ) + + if (!confirmed) return + + const deletedSet = new Set(selected) + const currentActiveId = props.activeSessionId + + let fallbackSessionId: string | undefined + if (currentActiveId && deletedSet.has(currentActiveId)) { + const ordered = getAllSessionIdsInOrder(props.threads) + const currentIndex = ordered.indexOf(currentActiveId) + + for (let i = Math.max(0, currentIndex); i < ordered.length; i++) { + const candidate = ordered[i] + if (candidate && !deletedSet.has(candidate)) { + fallbackSessionId = candidate + break + } + } + if (!fallbackSessionId) { + for (let i = currentIndex - 1; i >= 0; i--) { + const candidate = ordered[i] + if (candidate && !deletedSet.has(candidate)) { + fallbackSessionId = candidate + break + } + } + } + } + + let failed = 0 + for (const sessionId of selected) { + try { + // eslint-disable-next-line no-await-in-loop + await deleteSession(props.instanceId, sessionId) + } catch (error) { + failed += 1 + log.error(`Failed to delete session ${sessionId}:`, error) + } + } + + setSelectedSessionIds(new Set()) + + if (fallbackSessionId) { + setActiveSessionFromList(props.instanceId, fallbackSessionId) + } + + if (failed > 0) { + showToastNotification({ + message: t("sessionList.bulkDelete.error", { count: failed }), + variant: "error", + }) + } + } const SessionRow: Component<{ @@ -190,9 +376,31 @@ const SessionList: Component = (props) => { ? t("sessionList.status.needsInput") : statusLabel() - return ( -
+ const isSelected = () => selectedSessionIds().has(rowProps.sessionId) + const parentGroupState = createMemo(() => { + if (rowProps.isChild) { + return { checked: isSelected(), indeterminate: false, ids: [rowProps.sessionId] } + } + + const ids = getSelectableThreadIds(rowProps.sessionId) + const selected = selectedSessionIds() + const selectedInGroup = ids.reduce((count, id) => (selected.has(id) ? count + 1 : count), 0) + return { + checked: selectedInGroup > 0 && selectedInGroup === ids.length, + indeterminate: selectedInGroup > 0 && selectedInGroup < ids.length, + ids, + } + }) + + let rowCheckboxEl: HTMLInputElement | null = null + createEffect(() => { + if (!rowCheckboxEl) return + rowCheckboxEl.indeterminate = parentGroupState().indeterminate + }) + + return ( +
+
+ + 0}> +
+ + +
+
+
+ +
{props.headerContent ?? ( @@ -378,33 +651,33 @@ const SessionList: Component = (props) => {
-
listEl[1](el)}> +
listEl[1](el)}> - 0}> -
- + 0}> +
+ - {(thread) => { - const expanded = () => isSessionParentExpanded(props.instanceId, thread.parent.id) - return ( - <> - 0} - expanded={expanded()} - onToggleExpand={() => toggleSessionParentExpanded(props.instanceId, thread.parent.id)} - /> + {(thread) => { + const expanded = () => (normalizedQuery() ? true : isSessionParentExpanded(props.instanceId, thread.parent.id)) + return ( + <> + 0} + expanded={expanded()} + onToggleExpand={() => toggleSessionParentExpanded(props.instanceId, thread.parent.id)} + /> - 0}> - - {(child, index) => ( - - )} - - - - ) - }} + 0}> + + {(child, index) => ( + + )} + + + + ) + }}
diff --git a/packages/ui/src/lib/i18n/messages/en/session.ts b/packages/ui/src/lib/i18n/messages/en/session.ts index 784c411e..0cba5509 100644 --- a/packages/ui/src/lib/i18n/messages/en/session.ts +++ b/packages/ui/src/lib/i18n/messages/en/session.ts @@ -30,8 +30,27 @@ export const sessionMessages = { "sessionList.copyId.success": "Session ID copied", "sessionList.copyId.error": "Unable to copy session ID", "sessionList.delete.error": "Unable to delete session", + "sessionList.delete.title": "Delete session", + "sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.", + "sessionList.delete.confirmLabel": "Delete", + "sessionList.delete.cancelLabel": "Cancel", "sessionList.rename.error": "Unable to rename session", + "sessionList.filter.placeholder": "Search sessions…", + "sessionList.filter.ariaLabel": "Search sessions", + "sessionList.selection.selectAllLabel": "Select all", + "sessionList.selection.selectAllAriaLabel": "Select all sessions", + "sessionList.selection.clearLabel": "Clear", + "sessionList.selection.clearAriaLabel": "Clear selection", + "sessionList.selection.checkboxAriaLabel": "Select session", + "sessionList.bulkDelete.button": "Delete {count}", + "sessionList.bulkDelete.ariaLabel": "Delete {count} selected sessions", + "sessionList.bulkDelete.title": "Delete sessions", + "sessionList.bulkDelete.confirmMessage": "Delete {count} selected sessions? This cannot be undone.", + "sessionList.bulkDelete.confirmLabel": "Delete", + "sessionList.bulkDelete.cancelLabel": "Cancel", + "sessionList.bulkDelete.error": "Unable to delete {count} sessions", + "sessionRenameDialog.title": "Rename Session", "sessionRenameDialog.description.withLabel": "Update the title for \"{label}\".", "sessionRenameDialog.description.default": "Set a new title for this session.", diff --git a/packages/ui/src/lib/i18n/messages/es/session.ts b/packages/ui/src/lib/i18n/messages/es/session.ts index f4ab8ff7..ecdac0d7 100644 --- a/packages/ui/src/lib/i18n/messages/es/session.ts +++ b/packages/ui/src/lib/i18n/messages/es/session.ts @@ -30,8 +30,27 @@ export const sessionMessages = { "sessionList.copyId.success": "ID de sesión copiado", "sessionList.copyId.error": "No se pudo copiar el ID de sesión", "sessionList.delete.error": "No se pudo eliminar la sesión", + "sessionList.delete.title": "Eliminar sesión", + "sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.", + "sessionList.delete.confirmLabel": "Eliminar", + "sessionList.delete.cancelLabel": "Cancelar", "sessionList.rename.error": "No se pudo renombrar la sesión", + "sessionList.filter.placeholder": "Buscar sesiones…", + "sessionList.filter.ariaLabel": "Buscar sesiones", + "sessionList.selection.selectAllLabel": "Seleccionar todo", + "sessionList.selection.selectAllAriaLabel": "Seleccionar todas las sesiones", + "sessionList.selection.clearLabel": "Limpiar", + "sessionList.selection.clearAriaLabel": "Limpiar selección", + "sessionList.selection.checkboxAriaLabel": "Seleccionar sesión", + "sessionList.bulkDelete.button": "Eliminar {count}", + "sessionList.bulkDelete.ariaLabel": "Eliminar {count} sesiones seleccionadas", + "sessionList.bulkDelete.title": "Eliminar sesiones", + "sessionList.bulkDelete.confirmMessage": "¿Eliminar {count} sesiones seleccionadas? Esto no se puede deshacer.", + "sessionList.bulkDelete.confirmLabel": "Eliminar", + "sessionList.bulkDelete.cancelLabel": "Cancelar", + "sessionList.bulkDelete.error": "No se pudieron eliminar {count} sesiones", + "sessionRenameDialog.title": "Renombrar sesión", "sessionRenameDialog.description.withLabel": "Actualiza el título de \"{label}\".", "sessionRenameDialog.description.default": "Establece un nuevo título para esta sesión.", diff --git a/packages/ui/src/lib/i18n/messages/fr/session.ts b/packages/ui/src/lib/i18n/messages/fr/session.ts index 79b34762..7bee6007 100644 --- a/packages/ui/src/lib/i18n/messages/fr/session.ts +++ b/packages/ui/src/lib/i18n/messages/fr/session.ts @@ -30,8 +30,27 @@ export const sessionMessages = { "sessionList.copyId.success": "ID de session copié", "sessionList.copyId.error": "Impossible de copier l'ID de session", "sessionList.delete.error": "Impossible de supprimer la session", + "sessionList.delete.title": "Supprimer la session", + "sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.", + "sessionList.delete.confirmLabel": "Supprimer", + "sessionList.delete.cancelLabel": "Annuler", "sessionList.rename.error": "Impossible de renommer la session", + "sessionList.filter.placeholder": "Rechercher des sessions…", + "sessionList.filter.ariaLabel": "Rechercher des sessions", + "sessionList.selection.selectAllLabel": "Tout sélectionner", + "sessionList.selection.selectAllAriaLabel": "Sélectionner toutes les sessions", + "sessionList.selection.clearLabel": "Effacer", + "sessionList.selection.clearAriaLabel": "Effacer la sélection", + "sessionList.selection.checkboxAriaLabel": "Sélectionner la session", + "sessionList.bulkDelete.button": "Supprimer {count}", + "sessionList.bulkDelete.ariaLabel": "Supprimer {count} sessions sélectionnées", + "sessionList.bulkDelete.title": "Supprimer des sessions", + "sessionList.bulkDelete.confirmMessage": "Supprimer {count} sessions sélectionnées ? Cette action est irréversible.", + "sessionList.bulkDelete.confirmLabel": "Supprimer", + "sessionList.bulkDelete.cancelLabel": "Annuler", + "sessionList.bulkDelete.error": "Impossible de supprimer {count} sessions", + "sessionRenameDialog.title": "Renommer la session", "sessionRenameDialog.description.withLabel": "Mettre à jour le titre de \"{label}\".", "sessionRenameDialog.description.default": "Définir un nouveau titre pour cette session.", diff --git a/packages/ui/src/lib/i18n/messages/ja/session.ts b/packages/ui/src/lib/i18n/messages/ja/session.ts index 20342942..ca5e2d09 100644 --- a/packages/ui/src/lib/i18n/messages/ja/session.ts +++ b/packages/ui/src/lib/i18n/messages/ja/session.ts @@ -30,8 +30,27 @@ export const sessionMessages = { "sessionList.copyId.success": "セッション ID をコピーしました", "sessionList.copyId.error": "セッション ID をコピーできません", "sessionList.delete.error": "セッションを削除できません", + "sessionList.delete.title": "セッションを削除", + "sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。", + "sessionList.delete.confirmLabel": "削除", + "sessionList.delete.cancelLabel": "キャンセル", "sessionList.rename.error": "セッション名を変更できません", + "sessionList.filter.placeholder": "セッションを検索…", + "sessionList.filter.ariaLabel": "セッションを検索", + "sessionList.selection.selectAllLabel": "すべて選択", + "sessionList.selection.selectAllAriaLabel": "すべてのセッションを選択", + "sessionList.selection.clearLabel": "クリア", + "sessionList.selection.clearAriaLabel": "選択をクリア", + "sessionList.selection.checkboxAriaLabel": "セッションを選択", + "sessionList.bulkDelete.button": "{count} 件を削除", + "sessionList.bulkDelete.ariaLabel": "選択した {count} 件のセッションを削除", + "sessionList.bulkDelete.title": "セッションを削除", + "sessionList.bulkDelete.confirmMessage": "選択した {count} 件のセッションを削除しますか?この操作は元に戻せません。", + "sessionList.bulkDelete.confirmLabel": "削除", + "sessionList.bulkDelete.cancelLabel": "キャンセル", + "sessionList.bulkDelete.error": "{count} 件のセッションを削除できません", + "sessionRenameDialog.title": "セッション名を変更", "sessionRenameDialog.description.withLabel": "\"{label}\" のタイトルを更新します。", "sessionRenameDialog.description.default": "このセッションの新しいタイトルを設定します。", diff --git a/packages/ui/src/lib/i18n/messages/ru/session.ts b/packages/ui/src/lib/i18n/messages/ru/session.ts index eddb136c..f15194e2 100644 --- a/packages/ui/src/lib/i18n/messages/ru/session.ts +++ b/packages/ui/src/lib/i18n/messages/ru/session.ts @@ -30,8 +30,27 @@ export const sessionMessages = { "sessionList.copyId.success": "ID сессии скопирован", "sessionList.copyId.error": "Не удалось скопировать ID сессии", "sessionList.delete.error": "Не удалось удалить сессию", + "sessionList.delete.title": "Удалить сессию", + "sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.", + "sessionList.delete.confirmLabel": "Удалить", + "sessionList.delete.cancelLabel": "Отмена", "sessionList.rename.error": "Не удалось переименовать сессию", + "sessionList.filter.placeholder": "Поиск сессий…", + "sessionList.filter.ariaLabel": "Поиск сессий", + "sessionList.selection.selectAllLabel": "Выбрать все", + "sessionList.selection.selectAllAriaLabel": "Выбрать все сессии", + "sessionList.selection.clearLabel": "Очистить", + "sessionList.selection.clearAriaLabel": "Очистить выбор", + "sessionList.selection.checkboxAriaLabel": "Выбрать сессию", + "sessionList.bulkDelete.button": "Удалить {count}", + "sessionList.bulkDelete.ariaLabel": "Удалить {count} выбранных сессий", + "sessionList.bulkDelete.title": "Удалить сессии", + "sessionList.bulkDelete.confirmMessage": "Удалить {count} выбранных сессий? Это действие нельзя отменить.", + "sessionList.bulkDelete.confirmLabel": "Удалить", + "sessionList.bulkDelete.cancelLabel": "Отмена", + "sessionList.bulkDelete.error": "Не удалось удалить {count} сессий", + "sessionRenameDialog.title": "Переименовать сессию", "sessionRenameDialog.description.withLabel": "Обновите название для \"{label}\".", "sessionRenameDialog.description.default": "Установите новое название для этой сессии.", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/session.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/session.ts index effac4f2..3dfc79a8 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/session.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/session.ts @@ -30,8 +30,27 @@ export const sessionMessages = { "sessionList.copyId.success": "已复制会话 ID", "sessionList.copyId.error": "无法复制会话 ID", "sessionList.delete.error": "无法删除会话", + "sessionList.delete.title": "删除会话", + "sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。", + "sessionList.delete.confirmLabel": "删除", + "sessionList.delete.cancelLabel": "取消", "sessionList.rename.error": "无法重命名会话", + "sessionList.filter.placeholder": "搜索会话…", + "sessionList.filter.ariaLabel": "搜索会话", + "sessionList.selection.selectAllLabel": "全选", + "sessionList.selection.selectAllAriaLabel": "选择所有会话", + "sessionList.selection.clearLabel": "清除", + "sessionList.selection.clearAriaLabel": "清除选择", + "sessionList.selection.checkboxAriaLabel": "选择会话", + "sessionList.bulkDelete.button": "删除 {count}", + "sessionList.bulkDelete.ariaLabel": "删除已选择的 {count} 个会话", + "sessionList.bulkDelete.title": "删除会话", + "sessionList.bulkDelete.confirmMessage": "删除已选择的 {count} 个会话?此操作无法撤销。", + "sessionList.bulkDelete.confirmLabel": "删除", + "sessionList.bulkDelete.cancelLabel": "取消", + "sessionList.bulkDelete.error": "无法删除 {count} 个会话", + "sessionRenameDialog.title": "重命名会话", "sessionRenameDialog.description.withLabel": "更新“{label}”的标题。", "sessionRenameDialog.description.default": "为此会话设置新标题。", From 3522d3dff552ffbce98ba3bcf1c68a30b2e716c2 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sat, 31 Jan 2026 11:24:56 +0000 Subject: [PATCH 02/14] fix(electron): quit on last window close --- packages/electron-app/electron/main/main.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/electron-app/electron/main/main.ts b/packages/electron-app/electron/main/main.ts index 60240886..071c5620 100644 --- a/packages/electron-app/electron/main/main.ts +++ b/packages/electron-app/electron/main/main.ts @@ -505,7 +505,6 @@ app.on("before-quit", async (event) => { }) app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - app.quit() - } + // CodeNomad supports a single window; closing it should quit the app on all platforms. + app.quit() }) From 929e79befdb1a1bf8693375525172885d73334f9 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 2 Feb 2026 11:22:49 +0000 Subject: [PATCH 03/14] chore(license): add MIT license Clarifies usage and redistribution terms across the monorepo. --- LICENSE | 21 +++++++++++++++++++++ package.json | 1 + packages/cloudflare/package.json | 1 + packages/electron-app/package.json | 1 + packages/opencode-config/package.json | 1 + packages/server/package.json | 1 + packages/tauri-app/package.json | 1 + packages/tauri-app/src-tauri/Cargo.toml | 1 + packages/ui/package.json | 1 + 9 files changed, 29 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..64cabb59 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Neural Nomads + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json index 6dcd9d9a..96e56dfd 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.9.3", "private": true, "description": "CodeNomad monorepo workspace", + "license": "MIT", "workspaces": { "packages": [ "packages/server", diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index e678bc94..ec362fce 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,7 @@ { "name": "@codenomad/ui-host-worker", "private": true, + "license": "MIT", "type": "module", "scripts": { "build:manifest": "node ./scripts/build-manifest.mjs", diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index b3906dfe..118d83f6 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -2,6 +2,7 @@ "name": "@neuralnomads/codenomad-electron-app", "version": "0.9.3", "description": "CodeNomad - AI coding assistant", + "license": "MIT", "author": { "name": "Neural Nomads", "email": "codenomad@neuralnomads.ai" diff --git a/packages/opencode-config/package.json b/packages/opencode-config/package.json index 3ec5198a..edb8a6ae 100644 --- a/packages/opencode-config/package.json +++ b/packages/opencode-config/package.json @@ -2,6 +2,7 @@ "name": "@codenomad/opencode-config", "version": "0.5.0", "private": true, + "license": "MIT", "dependencies": { "@opencode-ai/plugin": "1.1.36" } diff --git a/packages/server/package.json b/packages/server/package.json index 26804ce8..9d9d17f3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -2,6 +2,7 @@ "name": "@neuralnomads/codenomad", "version": "0.9.3", "description": "CodeNomad Server", + "license": "MIT", "author": { "name": "Neural Nomads", "email": "codenomad@neuralnomads.ai" diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json index 69c7f685..1af212df 100644 --- a/packages/tauri-app/package.json +++ b/packages/tauri-app/package.json @@ -2,6 +2,7 @@ "name": "@codenomad/tauri-app", "version": "0.9.3", "private": true, + "license": "MIT", "scripts": { "dev": "tauri dev", "dev:ui": "npm run dev --workspace @codenomad/ui", diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml index 11e770ad..69626ad9 100644 --- a/packages/tauri-app/src-tauri/Cargo.toml +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -2,6 +2,7 @@ name = "codenomad-tauri" version = "0.1.0" edition = "2021" +license = "MIT" [build-dependencies] tauri-build = { version = "2.5.2", features = [] } diff --git a/packages/ui/package.json b/packages/ui/package.json index cc8aa45b..e5847147 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,6 +2,7 @@ "name": "@codenomad/ui", "version": "0.9.3", "private": true, + "license": "MIT", "type": "module", "scripts": { "dev": "vite dev", From de20b3adf317839a92ca796fa9a59d3b26302db0 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 15:07:05 +0000 Subject: [PATCH 04/14] fix(ui): allow collapsing active parent thread --- packages/ui/src/components/session-list.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index 2a8119a9..a529f221 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -120,9 +120,10 @@ const SessionList: Component = (props) => { const selectSession = (sessionId: string) => { const session = sessionStateSessions().get(props.instanceId)?.get(sessionId) - const parentId = session?.parentId ?? session?.id - if (parentId) { - ensureSessionParentExpanded(props.instanceId, parentId) + // If the user selects a child session, make sure its parent thread is expanded. + // For parent sessions we don't force expansion; user can collapse/expand freely. + if (session?.parentId) { + ensureSessionParentExpanded(props.instanceId, session.parentId) } props.onSelect(sessionId) @@ -525,6 +526,13 @@ const SessionList: Component = (props) => { }) createEffect(() => { + // Keep the active child session visible by ensuring its parent is expanded. + // Don't force-expanding when the active session itself is a parent lets users collapse it. + const activeId = props.activeSessionId + if (!activeId || activeId === "info") return + const activeSession = sessionStateSessions().get(props.instanceId)?.get(activeId) + if (!activeSession) return + if (!activeSession.parentId) return const parentId = activeParentId() if (!parentId) return ensureSessionParentExpanded(props.instanceId, parentId) From ea4c68712531d2c0eddb45795b016b9486d07d5e Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 15:08:24 +0000 Subject: [PATCH 05/14] chore: add MIT License --- package-lock.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package-lock.json b/package-lock.json index 597aa86f..27a2f2ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "codenomad-workspace", "version": "0.9.3", + "license": "MIT", "dependencies": { "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" @@ -7404,6 +7405,7 @@ "packages/electron-app": { "name": "@neuralnomads/codenomad-electron-app", "version": "0.9.3", + "license": "MIT", "dependencies": { "@codenomad/ui": "file:../ui", "@neuralnomads/codenomad": "file:../server" @@ -7438,6 +7440,7 @@ "packages/server": { "name": "@neuralnomads/codenomad", "version": "0.9.3", + "license": "MIT", "dependencies": { "@fastify/cors": "^8.5.0", "@fastify/reply-from": "^9.8.0", @@ -7475,6 +7478,7 @@ "packages/tauri-app": { "name": "@codenomad/tauri-app", "version": "0.9.3", + "license": "MIT", "devDependencies": { "@tauri-apps/cli": "^2.9.4" } @@ -7482,6 +7486,7 @@ "packages/ui": { "name": "@codenomad/ui", "version": "0.9.3", + "license": "MIT", "dependencies": { "@git-diff-view/solid": "^0.0.8", "@kobalte/core": "0.13.11", From a2127a11ac9a2b43e9c658139f60f8252226fb77 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 15:22:49 +0000 Subject: [PATCH 06/14] fix(server): include symlink directories in listings Fixes https://github.com/NeuralNomadsAI/CodeNomad/issues/106 --- packages/server/src/filesystem/browser.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/server/src/filesystem/browser.ts b/packages/server/src/filesystem/browser.ts index e5820f3d..d2a8065d 100644 --- a/packages/server/src/filesystem/browser.ts +++ b/packages/server/src/filesystem/browser.ts @@ -222,20 +222,18 @@ export class FileSystemBrowser { const results: FileSystemEntry[] = [] for (const entry of dirents) { - if (!options.includeFiles && !entry.isDirectory()) { - continue - } - const absoluteEntryPath = path.join(directory, entry.name) let stats: fs.Stats try { + // Use fs.statSync (not Dirent.isDirectory) so symlinks to directories + // are treated as directories in directory-only listings. stats = fs.statSync(absoluteEntryPath) } catch { // Skip entries we cannot stat (insufficient permissions, etc.) continue } - const isDirectory = entry.isDirectory() + const isDirectory = stats.isDirectory() if (!options.includeFiles && !isDirectory) { continue } From 17a3e43ac75091c21a069fb7bc3788705a00f28a Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 16:49:42 +0000 Subject: [PATCH 07/14] feat(ui): add system/light/dark theme toggle Add a 3-state theme toggle in folder selection and instance tabs, and update tokens/styles so light mode has readable contrast. Sync MUI surfaces and Shiki highlighting to CSS variables to prevent stale colors when switching themes. --- .../ui/src/components/code-block-inline.tsx | 2 +- .../src/components/folder-selection-view.tsx | 10 ++- packages/ui/src/components/instance-tabs.tsx | 2 + .../components/instance/instance-shell2.tsx | 14 ++-- .../ui/src/components/message-list-header.tsx | 2 +- packages/ui/src/components/message-part.tsx | 18 ++-- .../session/context-usage-panel.tsx | 8 +- .../ui/src/components/theme-mode-toggle.tsx | 39 +++++++++ packages/ui/src/lib/i18n/messages/en/app.ts | 6 ++ packages/ui/src/lib/markdown.ts | 14 ++-- packages/ui/src/lib/theme.tsx | 82 ++++++++++++++----- packages/ui/src/renderer/loading/loading.css | 74 +++++++++++++---- .../components/permission-notification.css | 6 +- .../src/styles/messaging/message-section.css | 2 +- .../src/styles/messaging/message-timeline.css | 24 +++--- .../ui/src/styles/messaging/prompt-input.css | 16 ++-- .../ui/src/styles/messaging/tool-call.css | 32 ++++---- packages/ui/src/styles/tokens.css | 54 +++++++++--- 18 files changed, 288 insertions(+), 117 deletions(-) create mode 100644 packages/ui/src/components/theme-mode-toggle.tsx diff --git a/packages/ui/src/components/code-block-inline.tsx b/packages/ui/src/components/code-block-inline.tsx index 92b3f935..dc26c153 100644 --- a/packages/ui/src/components/code-block-inline.tsx +++ b/packages/ui/src/components/code-block-inline.tsx @@ -55,7 +55,7 @@ export function CodeBlockInline(props: CodeBlockInlineProps) { const highlighted = highlighter.codeToHtml(props.code, { lang: props.language as CodeToHtmlOptions["lang"], - theme: isDark() ? "github-dark" : "github-light", + theme: isDark() ? "github-dark" : "github-light-high-contrast", }) setHtml(highlighted) } catch { diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index fef0c1f3..a77e3547 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -5,6 +5,7 @@ import { useConfig } from "../stores/preferences" import AdvancedSettingsModal from "./advanced-settings-modal" import DirectoryBrowserDialog from "./directory-browser-dialog" import Kbd from "./kbd" +import { ThemeModeToggle } from "./theme-mode-toggle" import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions" import VersionPill from "./version-pill" import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons" @@ -313,8 +314,9 @@ const FolderSelectionView: Component = (props) => {
- -
+
+ + -
- + +
{t("folderSelection.logoAlt")} diff --git a/packages/ui/src/components/instance-tabs.tsx b/packages/ui/src/components/instance-tabs.tsx index 2a244cbf..c525de60 100644 --- a/packages/ui/src/components/instance-tabs.tsx +++ b/packages/ui/src/components/instance-tabs.tsx @@ -5,6 +5,7 @@ import KeyboardHint from "./keyboard-hint" import { Plus, MonitorUp } from "lucide-solid" import { keyboardRegistry } from "../lib/keyboard-registry" import { useI18n } from "../lib/i18n" +import { ThemeModeToggle } from "./theme-mode-toggle" interface InstanceTabsProps { instances: Map @@ -52,6 +53,7 @@ const InstanceTabs: Component = (props) => { />
+
-
+
= (props) => { return (
-
- +
+ {t("instanceShell.rightPanel.title")}
@@ -1331,13 +1331,13 @@ const InstanceShell2: Component = (props) => {
- + {t("instanceShell.metrics.usedLabel")} {formattedUsedTokens()}
- + {t("instanceShell.metrics.availableLabel")} {formattedAvailableTokens()} @@ -1361,13 +1361,13 @@ const InstanceShell2: Component = (props) => {
- + {t("instanceShell.metrics.usedLabel")} {formattedUsedTokens()}
- + {t("instanceShell.metrics.availableLabel")} {formattedAvailableTokens()} diff --git a/packages/ui/src/components/message-list-header.tsx b/packages/ui/src/components/message-list-header.tsx index 78a1d7b4..dab3d361 100644 --- a/packages/ui/src/components/message-list-header.tsx +++ b/packages/ui/src/components/message-list-header.tsx @@ -3,7 +3,7 @@ import Kbd from "./kbd" import { useI18n } from "../lib/i18n" const METRIC_CHIP_CLASS = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary" -const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-primary/70" +const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted" interface MessageListHeaderProps { usedTokens: number diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 50e7759f..1256d353 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -103,15 +103,15 @@ interface MessagePartProps {
- {plainTextContent()}} - > - {plainTextContent()}} + > + diff --git a/packages/ui/src/components/session/context-usage-panel.tsx b/packages/ui/src/components/session/context-usage-panel.tsx index 004709a9..d0fb64b4 100644 --- a/packages/ui/src/components/session/context-usage-panel.tsx +++ b/packages/ui/src/components/session/context-usage-panel.tsx @@ -9,8 +9,8 @@ interface ContextUsagePanelProps { } const chipClass = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary" -const chipLabelClass = "uppercase text-[10px] tracking-wide text-primary/70" -const headingClass = "text-xs font-semibold text-primary/70 uppercase tracking-wide" +const chipLabelClass = "uppercase text-[10px] tracking-wide text-muted" +const headingClass = "text-xs font-semibold text-muted uppercase tracking-wide" const ContextUsagePanel: Component = (props) => { const { t } = useI18n() @@ -49,7 +49,7 @@ const ContextUsagePanel: Component = (props) => { return (
-
+
{t("contextUsagePanel.headings.tokens")}
{t("contextUsagePanel.labels.input")} @@ -65,7 +65,7 @@ const ContextUsagePanel: Component = (props) => {
-
+
{t("contextUsagePanel.headings.context")}
{t("contextUsagePanel.labels.used")} diff --git a/packages/ui/src/components/theme-mode-toggle.tsx b/packages/ui/src/components/theme-mode-toggle.tsx new file mode 100644 index 00000000..f5246fff --- /dev/null +++ b/packages/ui/src/components/theme-mode-toggle.tsx @@ -0,0 +1,39 @@ +import { createMemo, type Component } from "solid-js" +import { Laptop, Moon, Sun } from "lucide-solid" +import { useI18n } from "../lib/i18n" +import { useTheme } from "../lib/theme" + +interface ThemeModeToggleProps { + class?: string +} + +export const ThemeModeToggle: Component = (props) => { + const { t } = useI18n() + const { themeMode, cycleThemeMode } = useTheme() + + const modeLabel = () => { + const mode = themeMode() + if (mode === "system") return t("theme.mode.system") + if (mode === "light") return t("theme.mode.light") + return t("theme.mode.dark") + } + + const icon = createMemo(() => { + const mode = themeMode() + if (mode === "system") return + if (mode === "light") return + return + }) + + return ( + + ) +} diff --git a/packages/ui/src/lib/i18n/messages/en/app.ts b/packages/ui/src/lib/i18n/messages/en/app.ts index 3a9345d3..ec1600e6 100644 --- a/packages/ui/src/lib/i18n/messages/en/app.ts +++ b/packages/ui/src/lib/i18n/messages/en/app.ts @@ -29,4 +29,10 @@ export const appMessages = { "releases.uiUpdated.title": "UI updated", "releases.uiUpdated.message": "UI is now updated to {version}.", + + "theme.mode.system": "System", + "theme.mode.light": "Light", + "theme.mode.dark": "Dark", + "theme.toggle.title": "Theme: {mode}", + "theme.toggle.ariaLabel": "Theme: {mode}", } as const diff --git a/packages/ui/src/lib/markdown.ts b/packages/ui/src/lib/markdown.ts index 87cdd10e..a8287d15 100644 --- a/packages/ui/src/lib/markdown.ts +++ b/packages/ui/src/lib/markdown.ts @@ -91,7 +91,7 @@ async function getOrCreateHighlighter() { // Create highlighter with no preloaded languages highlighterPromise = createHighlighter({ - themes: ["github-light", "github-dark"], + themes: ["github-light", "github-light-high-contrast", "github-dark"], langs: [], }) @@ -242,9 +242,9 @@ async function runLanguageLoadQueue() { } function setupRenderer(isDark: boolean) { - if (!highlighter || rendererSetup) return - currentTheme = isDark ? "dark" : "light" + if (!highlighter) return + if (rendererSetup) return marked.setOptions({ breaks: true, @@ -296,10 +296,10 @@ function setupRenderer(isDark: boolean) { // Use highlighting if language is loaded, otherwise fall back to plain code if (loadedLanguages.has(langKey)) { try { - const html = highlighter!.codeToHtml(decodedCode, { - lang: langKey, - theme: currentTheme === "dark" ? "github-dark" : "github-light", - }) + const html = highlighter!.codeToHtml(decodedCode, { + lang: langKey, + theme: currentTheme === "dark" ? "github-dark" : "github-light-high-contrast", + }) return `
${header}${html}
` } catch { // Fall through to plain code if highlighting fails diff --git a/packages/ui/src/lib/theme.tsx b/packages/ui/src/lib/theme.tsx index 7bff6d33..654bb772 100644 --- a/packages/ui/src/lib/theme.tsx +++ b/packages/ui/src/lib/theme.tsx @@ -3,22 +3,24 @@ import { createTheme, ThemeProvider as MuiThemeProvider } from "@suid/material/s import CssBaseline from "@suid/material/CssBaseline" import { useConfig } from "../stores/preferences" +export type ThemeMode = "system" | "light" | "dark" + interface ThemeContextValue { isDark: () => boolean - toggleTheme: () => void - setTheme: (dark: boolean) => void + themeMode: () => ThemeMode + setThemeMode: (mode: ThemeMode) => void + cycleThemeMode: () => void } const ThemeContext = createContext() -function applyTheme(dark: boolean) { +function applyThemeMode(mode: ThemeMode) { if (typeof document === "undefined") return - if (dark) { - document.documentElement.setAttribute("data-theme", "dark") + if (mode === "system") { + document.documentElement.removeAttribute("data-theme") return } - - document.documentElement.removeAttribute("data-theme") + document.documentElement.setAttribute("data-theme", mode) } interface ResolvedPaletteColors { @@ -76,24 +78,45 @@ const resolvePaletteColors = (dark: boolean): ResolvedPaletteColors => { export function ThemeProvider(props: { children: JSX.Element }) { const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null - const { themePreference, setThemePreference } = useConfig() + const { themePreference } = useConfig() const [isDark, setIsDarkSignal] = createSignal(true) + const [themeMode, setThemeModeSignal] = createSignal(themePreference()) + const [hasUserOverride, setHasUserOverride] = createSignal(false) + const [themeRevision, setThemeRevision] = createSignal(0) const resolveDarkTheme = () => { - themePreference() - return true + const mode = themeMode() + if (mode === "dark") return true + if (mode === "light") return false + return mediaQuery?.matches ?? false } const applyResolvedTheme = () => { + const mode = themeMode() const dark = resolveDarkTheme() + if (mode === "system") { + applyThemeMode("system") + } else { + applyThemeMode(mode) + } setIsDarkSignal(dark) - applyTheme(dark) + if (typeof window !== "undefined") { + requestAnimationFrame(() => setThemeRevision((v) => v + 1)) + } else { + setThemeRevision((v) => v + 1) + } } createEffect(() => { applyResolvedTheme() }) + createEffect(() => { + const preference = themePreference() + if (hasUserOverride()) return + setThemeModeSignal(preference) + }) + onMount(() => { if (!mediaQuery) return const handleSystemThemeChange = () => { @@ -107,15 +130,21 @@ export function ThemeProvider(props: { children: JSX.Element }) { } }) - const setTheme = (_dark: boolean) => { - setThemePreference("dark") + const setThemeMode = (mode: ThemeMode) => { + setHasUserOverride(true) + setThemeModeSignal(mode) + // Persistence is intentionally implemented later. + // When we wire it up, this should call `setThemePreference(mode)`. } - const toggleTheme = () => { - setTheme(true) + const cycleThemeMode = () => { + const current = themeMode() + const next: ThemeMode = current === "system" ? "light" : current === "light" ? "dark" : "system" + setThemeMode(next) } const muiTheme = createMemo(() => { + themeRevision() const paletteColors = resolvePaletteColors(isDark()) return createTheme({ palette: { @@ -144,21 +173,32 @@ export function ThemeProvider(props: { children: JSX.Element }) { borderRadius: 8, }, components: { + MuiIconButton: { + styleOverrides: { + root: { + color: "inherit", + "&.Mui-disabled": { + color: "var(--text-muted)", + opacity: 0.55, + }, + }, + }, + }, MuiDrawer: { styleOverrides: { paper: { - backgroundColor: paletteColors.backgroundPaper, - color: paletteColors.textPrimary, + backgroundColor: "var(--surface-secondary)", + color: "var(--text-primary)", }, }, }, MuiAppBar: { styleOverrides: { root: { - backgroundColor: paletteColors.backgroundPaper, - color: paletteColors.textPrimary, + backgroundColor: "var(--surface-secondary)", + color: "var(--text-primary)", boxShadow: "none", - borderBottom: `1px solid ${paletteColors.divider}`, + borderBottom: "1px solid var(--border-base)", zIndex: 10, }, }, @@ -175,7 +215,7 @@ export function ThemeProvider(props: { children: JSX.Element }) { }) return ( - + {props.children} diff --git a/packages/ui/src/renderer/loading/loading.css b/packages/ui/src/renderer/loading/loading.css index 532e1543..c26c8ea7 100644 --- a/packages/ui/src/renderer/loading/loading.css +++ b/packages/ui/src/renderer/loading/loading.css @@ -1,12 +1,12 @@ :root { - color-scheme: dark; + color-scheme: light dark; } body { margin: 0; min-height: 100vh; - background-color: var(--surface-base, #0f141f); - color: var(--text-primary, #cfd4dc); + background-color: var(--surface-base, #ffffff); + color: var(--text-primary, #1a1a1a); font-family: var(--font-family-sans, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif); display: flex; align-items: center; @@ -34,7 +34,7 @@ button { .loading-logo { width: 180px; height: auto; - filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.45)); + filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.18)); } .loading-heading { @@ -47,13 +47,13 @@ button { font-size: 2.8rem; font-weight: 600; margin: 0; - color: var(--text-primary, #f4f6fb); + color: var(--text-primary, #1a1a1a); } .loading-status { margin: 0; font-size: 1rem; - color: var(--text-muted, #aeb3c4); + color: var(--text-muted, #666666); } .loading-card { @@ -62,9 +62,9 @@ button { max-width: 420px; padding: 22px; border-radius: 18px; - background: rgba(13, 16, 24, 0.85); - border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 25px 60px rgba(0, 0, 0, 0.55); + background: var(--surface-secondary, #f5f5f5); + border: 1px solid var(--border-base, #e0e0e0); + box-shadow: var(--panel-shadow-strong, 0 25px 60px rgba(0, 0, 0, 0.16)); } .loading-row { @@ -79,28 +79,74 @@ button { width: 20px; height: 20px; border-radius: 50%; - border: 2px solid rgba(255, 255, 255, 0.18); - border-top-color: #6ce3ff; + border: 2px solid color-mix(in srgb, var(--text-primary, #1a1a1a) 14%, transparent); + border-top-color: var(--accent-primary, #0066ff); animation: spin 0.9s linear infinite; } .phrase-controls { margin-top: 12px; font-size: 0.9rem; - color: var(--text-muted, #8f96a9); + color: var(--text-muted, #666666); } .phrase-controls button { - color: #8fb5ff; + color: var(--accent-primary, #0066ff); cursor: pointer; } .loading-error { margin-top: 12px; - color: #ff9ea9; + color: var(--status-error, #dc2626); font-size: 0.95rem; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + color-scheme: dark; + } + + :root:not([data-theme="light"]) body { + background-color: var(--surface-base, #0f141f); + color: var(--text-primary, #cfd4dc); + } + + :root:not([data-theme="light"]) .loading-logo { + filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.45)); + } + + :root:not([data-theme="light"]) .loading-title { + color: var(--text-primary, #f4f6fb); + } + + :root:not([data-theme="light"]) .loading-status { + color: var(--text-muted, #aeb3c4); + } + + :root:not([data-theme="light"]) .loading-card { + background: rgba(13, 16, 24, 0.85); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 25px 60px rgba(0, 0, 0, 0.55); + } + + :root:not([data-theme="light"]) .spinner { + border: 2px solid rgba(255, 255, 255, 0.18); + border-top-color: var(--accent-primary, #6ce3ff); + } + + :root:not([data-theme="light"]) .phrase-controls { + color: var(--text-muted, #8f96a9); + } + + :root:not([data-theme="light"]) .loading-error { + color: var(--status-error, #ff9ea9); + } +} + +:root[data-theme="dark"] { + color-scheme: dark; +} + @keyframes spin { from { transform: rotate(0deg); diff --git a/packages/ui/src/styles/components/permission-notification.css b/packages/ui/src/styles/components/permission-notification.css index ccffd2c5..0d990083 100644 --- a/packages/ui/src/styles/components/permission-notification.css +++ b/packages/ui/src/styles/components/permission-notification.css @@ -35,7 +35,7 @@ .permission-center-modal-backdrop { position: fixed; inset: 0; - background: color-mix(in srgb, var(--text-inverted) 55%, transparent); + background: var(--overlay-scrim); backdrop-filter: blur(4px); display: flex; align-items: center; @@ -52,7 +52,7 @@ border-radius: var(--radius-xl); border: 1px solid var(--border-base); background: var(--surface-base); - box-shadow: var(--panel-shadow, 0 12px 32px rgba(0, 0, 0, 0.25)); + box-shadow: var(--panel-shadow-strong); overflow: hidden; } @@ -234,4 +234,4 @@ max-height: none; border-radius: 0; } -} \ No newline at end of file +} diff --git a/packages/ui/src/styles/messaging/message-section.css b/packages/ui/src/styles/messaging/message-section.css index 7cb94733..1579fc3c 100644 --- a/packages/ui/src/styles/messaging/message-section.css +++ b/packages/ui/src/styles/messaging/message-section.css @@ -244,7 +244,7 @@ border-radius: 9999px; border: 1px solid var(--list-item-highlight-border); background-color: var(--list-item-highlight-bg-solid); - box-shadow: var(--panel-shadow, 0 4px 16px rgba(0, 0, 0, 0.2)); + box-shadow: var(--panel-shadow); overflow: hidden; } diff --git a/packages/ui/src/styles/messaging/message-timeline.css b/packages/ui/src/styles/messaging/message-timeline.css index 4d8fda6a..9fe5bc65 100644 --- a/packages/ui/src/styles/messaging/message-timeline.css +++ b/packages/ui/src/styles/messaging/message-timeline.css @@ -69,7 +69,7 @@ overflow-y: auto; border-radius: 8px; background-color: var(--surface-base); - box-shadow: var(--panel-shadow, 0 6px 24px rgba(0, 0, 0, 0.2)); + box-shadow: var(--panel-shadow); } .message-timeline::-webkit-scrollbar { @@ -104,10 +104,10 @@ .message-timeline-segment-active { border-color: transparent; - background-color: #0f5b44; - color: #fff; + background-color: var(--timeline-segment-active-bg); + color: var(--timeline-segment-active-text); font-weight: 700; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); + box-shadow: var(--timeline-segment-active-ring); } .message-timeline-segment:hover, @@ -121,10 +121,10 @@ .message-timeline-segment-active, .message-timeline-segment-active:hover, .message-timeline-segment-active:focus-visible { - background-color: #0f5b44; - color: #fff; + background-color: var(--timeline-segment-active-bg); + color: var(--timeline-segment-active-text); transform: none; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); + box-shadow: var(--timeline-segment-active-ring); } .message-timeline-segment:focus-visible { @@ -167,7 +167,7 @@ border-color: var(--session-status-permission-fg) !important; color: var(--session-status-permission-fg) !important; transform: none; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); + box-shadow: var(--timeline-segment-active-ring); } .message-timeline-compaction-auto { @@ -181,11 +181,11 @@ } .message-timeline-segment-active { - background-color: #0f5b44 !important; + background-color: var(--timeline-segment-active-bg) !important; border-color: transparent !important; - color: #fff !important; + color: var(--timeline-segment-active-text) !important; font-weight: 700; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); + box-shadow: var(--timeline-segment-active-ring); } .message-timeline-label { @@ -221,7 +221,7 @@ border-radius: 8px; border: 1px solid var(--border-base); background-color: var(--surface-base); - box-shadow: var(--panel-shadow, 0 12px 32px rgba(0, 0, 0, 0.25)); + box-shadow: var(--panel-shadow-strong); padding: 0.75rem; } diff --git a/packages/ui/src/styles/messaging/prompt-input.css b/packages/ui/src/styles/messaging/prompt-input.css index b0c87e5c..b5507031 100644 --- a/packages/ui/src/styles/messaging/prompt-input.css +++ b/packages/ui/src/styles/messaging/prompt-input.css @@ -97,7 +97,7 @@ .prompt-history-button { @apply w-7 h-7 flex items-center justify-center rounded-md; color: var(--text-muted); - background-color: rgba(15, 23, 42, 0.04); + background-color: var(--control-ghost-bg); transition: background-color 0.15s ease, color 0.15s ease; padding: 0; flex-shrink: 0; @@ -143,7 +143,7 @@ .prompt-input.shell-mode { border-color: var(--status-success); - box-shadow: inset 0 0 0 1px rgba(76, 175, 80, 0.4); + box-shadow: inset 0 0 0 1px var(--status-success-ring); } .prompt-input:focus { @@ -152,7 +152,7 @@ .prompt-input.shell-mode:focus { border-color: var(--status-success); - box-shadow: inset 0 0 0 1px rgba(76, 175, 80, 0.4); + box-shadow: inset 0 0 0 1px var(--status-success-ring); } .prompt-input:disabled { @@ -165,17 +165,17 @@ .stop-button { @apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0; - background-color: rgba(239, 68, 68, 0.85); - color: var(--text-inverted); + background-color: var(--button-danger-bg); + color: var(--button-danger-text); } .stop-button:hover:not(:disabled) { - background-color: rgba(239, 68, 68, 0.9); + background-color: var(--button-danger-hover-bg); @apply opacity-95 scale-105; } .stop-button:active:not(:disabled) { - background-color: rgba(239, 68, 68, 1); + background-color: var(--button-danger-active-bg); @apply scale-95; } @@ -260,7 +260,7 @@ background-color: var(--surface-base); border: 1px solid var(--border-base); border-radius: 10px; - box-shadow: 0 16px 40px rgba(15, 23, 42, 0.25); + box-shadow: var(--popover-shadow); z-index: 20; } diff --git a/packages/ui/src/styles/messaging/tool-call.css b/packages/ui/src/styles/messaging/tool-call.css index c0affd09..f39f470a 100644 --- a/packages/ui/src/styles/messaging/tool-call.css +++ b/packages/ui/src/styles/messaging/tool-call.css @@ -11,7 +11,7 @@ .tool-call-header-label { @apply flex items-center justify-between gap-2 font-semibold text-sm; - color: var(--message-tool-border); + color: var(--text-primary); margin-bottom: 1px; } @@ -22,7 +22,7 @@ .tool-call-header-button { background-color: transparent; border: 1px solid var(--border-base); - color: var(--message-tool-border); + color: var(--text-secondary); padding: 0.15rem 0.75rem; border-radius: 0.375rem; font-size: 0.75rem; @@ -162,7 +162,7 @@ .tool-call-preview-label { @apply text-xs font-semibold uppercase tracking-wide; - color: var(--text-muted); + color: var(--text-secondary); letter-spacing: 0.5px; } @@ -170,7 +170,7 @@ font-family: var(--font-family-mono); font-size: var(--font-size-xs); line-height: var(--line-height-tight); - color: var(--text-muted); + color: var(--text-secondary); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; @@ -231,7 +231,7 @@ .tool-call-diff-toolbar-label { font-size: 11px; - color: var(--text-muted); + color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.08em; } @@ -244,7 +244,7 @@ @apply border text-xs font-semibold px-3 py-1 rounded transition-all duration-150; border-color: var(--border-base); background-color: transparent; - color: var(--text-muted); + color: var(--text-secondary); } .tool-call-diff-mode-button:hover { @@ -388,7 +388,7 @@ .tool-call-diff-viewer .diff-line-old-num, .tool-call-diff-viewer .diff-line-new-num, .tool-call-diff-viewer .diff-line-num { - color: var(--text-muted); + color: var(--text-secondary); font-size: var(--font-size-xs); } @@ -450,7 +450,7 @@ font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); margin-bottom: 4px; - color: var(--text-muted); + color: var(--text-secondary); } .tool-call-diagnostics { @@ -472,7 +472,7 @@ @apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left; font-family: var(--font-family-mono); font-size: 13px; - color: var(--message-tool-border); + color: var(--text-primary); background-color: var(--surface-code); } @@ -496,7 +496,7 @@ @apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left; font-family: var(--font-family-mono); font-size: 13px; - color: var(--message-tool-border); + color: var(--text-primary); background-color: var(--surface-code); } @@ -535,7 +535,7 @@ .tool-call-diagnostics-caret { font-size: 12px; - color: var(--text-muted); + color: var(--text-secondary); } .tool-call-diagnostics { @@ -615,7 +615,8 @@ .tool-call-section pre { margin: 0; padding: 8px; - background-color: var(--surface-base); + background-color: var(--surface-code); + border: 1px solid var(--border-base); border-radius: 0px; overflow-x: auto; max-height: var(--tool-call-max-height-compact, calc(25 * 1.4em)); @@ -649,7 +650,7 @@ .tool-call-pending-message { @apply flex items-center gap-2 p-3 text-xs italic; - color: var(--text-muted); + color: var(--text-secondary); } .tool-call-emoji { @@ -659,7 +660,7 @@ .tool-call-action-button { @apply border text-xs font-semibold px-3 py-1 rounded transition-colors h-8 flex items-center; border-color: var(--border-base); - color: var(--text-muted); + color: var(--text-secondary); background-color: transparent; } @@ -679,7 +680,7 @@ } .tool-call-content { - background-color: var(--surface-secondary); + background-color: var(--surface-code); border: 1px solid var(--border-base); border-radius: 0; padding: 8px 12px; @@ -688,6 +689,7 @@ line-height: var(--line-height-tight); overflow-x: auto; margin: 0; + color: var(--text-primary); } .tool-call-content code { diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index 95c5d768..4f8763df 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -1,9 +1,10 @@ :root { + color-scheme: light; /* Surface tokens */ --surface-base: #ffffff; --surface-secondary: #f5f5f5; - --surface-muted: #f8f9fa; - --surface-code: #f8f8f8; + --surface-muted: #f8fafc; + --surface-code: #f1f5f9; --surface-hover: #e0e0e0; /* Border tokens */ @@ -12,9 +13,9 @@ --border-muted: #e0e0e0; /* Text tokens */ - --text-primary: #1a1a1a; - --text-secondary: #666666; - --text-muted: #666666; + --text-primary: #111827; + --text-secondary: #334155; + --text-muted: #475569; --text-inverted: #ffffff; /* Accent tokens */ @@ -27,13 +28,13 @@ --status-warning: #ff9800; /* Message-specific tokens */ - --message-user-bg: var(--surface-secondary); + --message-user-bg: color-mix(in oklab, var(--surface-secondary) 88%, var(--message-user-border)); --message-user-border: #2196f3; --message-assistant-bg: var(--message-tool-bg); --message-assistant-border: #f59e0b; - --message-tool-bg: #f8f9fa; - --message-tool-border: #6c757d; + --message-tool-bg: #eef2f7; + --message-tool-border: #64748b; /* Session list selection tints */ --session-user-active-bg: color-mix(in oklab, var(--surface-secondary) 85%, var(--message-user-border)); @@ -71,11 +72,14 @@ --folder-card-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); --folder-card-radius: 16px; --dropdown-highlight-bg: rgba(0, 102, 255, 0.1); - --dropdown-highlight-text: var(--text-inverted); + --dropdown-highlight-text: var(--text-primary); --selection-highlight-bg: rgba(0, 102, 255, 0.12); --selection-highlight-strong-bg: rgba(0, 102, 255, 0.18); --overlay-scrim: rgba(0, 0, 0, 0.5); --scroll-elevation-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); + --panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); + --panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.18); + --popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.18); --message-error-bg: rgba(244, 67, 54, 0.1); --message-error-bg-strong: rgba(244, 67, 54, 0.15); --danger-soft-bg: rgba(239, 68, 68, 0.1); @@ -86,6 +90,18 @@ --log-level-default: var(--text-primary); --focus-ring-color: var(--accent-primary); --focus-ring-offset: var(--surface-base); + --control-ghost-bg: color-mix(in oklab, var(--text-primary) 6%, transparent); + --status-success-ring: color-mix(in oklab, var(--status-success) 45%, transparent); + --border-critical: var(--status-error); + + --timeline-segment-active-bg: #0f5b44; + --timeline-segment-active-text: #ffffff; + --timeline-segment-active-ring: inset 0 0 0 1px rgba(0, 0, 0, 0.22); + + --button-danger-bg: color-mix(in oklab, var(--status-error) 85%, var(--surface-base)); + --button-danger-hover-bg: color-mix(in oklab, var(--status-error) 90%, var(--surface-base)); + --button-danger-active-bg: color-mix(in oklab, var(--status-error) 95%, var(--surface-base)); + --button-danger-text: #ffffff; --kbd-bg: var(--surface-secondary); --kbd-border: var(--border-base); --kbd-text: var(--text-primary); @@ -151,7 +167,8 @@ } @media (prefers-color-scheme: dark) { - :root { + :root:not([data-theme]) { + color-scheme: dark; /* Surface tokens */ --surface-base: #1a1a1a; --surface-secondary: #2a2a2a; @@ -225,6 +242,14 @@ --kbd-bg: var(--surface-secondary); --kbd-border: var(--border-base); --kbd-text: var(--text-primary); + --panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); + --panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.45); + --popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.55); + --border-critical: var(--status-error); + --timeline-segment-active-bg: #0f5b44; + --timeline-segment-active-text: #ffffff; + --timeline-segment-active-ring: inset 0 0 0 1px rgba(0, 0, 0, 0.35); + --button-danger-text: #ffffff; --button-primary-bg: #3f3f46; --button-primary-hover-bg: #52525b; --button-primary-text: #f5f6f8; @@ -306,6 +331,7 @@ } [data-theme="dark"] { + color-scheme: dark; /* Surface tokens */ --surface-base: #1a1a1a; --surface-secondary: #2a2a2a; @@ -379,6 +405,14 @@ --selection-highlight-strong-bg: rgba(0, 128, 255, 0.28); --overlay-scrim: rgba(0, 0, 0, 0.6); --scroll-elevation-shadow: 0 10px 25px rgba(0, 0, 0, 0.35); + --panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); + --panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.45); + --popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.55); + --border-critical: var(--status-error); + --timeline-segment-active-bg: #0f5b44; + --timeline-segment-active-text: #ffffff; + --timeline-segment-active-ring: inset 0 0 0 1px rgba(0, 0, 0, 0.35); + --button-danger-text: #ffffff; --message-error-bg: rgba(244, 67, 54, 0.12); --message-error-bg-strong: rgba(244, 67, 54, 0.2); --danger-soft-bg: rgba(244, 67, 54, 0.16); From aab06924032022fe3bc101efdfae4c5718635957 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 17:37:02 +0000 Subject: [PATCH 08/14] fix(ui): tune light mode contrast --- .../components/instance/instance-shell2.tsx | 3 +- packages/ui/src/components/markdown.tsx | 7 +- packages/ui/src/lib/markdown.ts | 4 + .../ui/src/styles/messaging/message-base.css | 8 +- .../src/styles/messaging/message-timeline.css | 4 +- .../ui/src/styles/messaging/prompt-input.css | 3 +- .../ui/src/styles/messaging/tool-call.css | 75 +++++++++++++------ .../src/styles/messaging/tool-call/task.css | 6 +- .../src/styles/messaging/tool-call/todo.css | 4 +- packages/ui/src/styles/panels/tabs.css | 17 +++++ packages/ui/src/styles/tokens.css | 10 ++- 11 files changed, 101 insertions(+), 40 deletions(-) diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 7675b586..68f76bc8 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -15,7 +15,6 @@ import { Accordion } from "@kobalte/core" import { ChevronDown, TerminalSquare, Trash2, XOctagon } from "lucide-solid" import AppBar from "@suid/material/AppBar" import Box from "@suid/material/Box" -import Divider from "@suid/material/Divider" import Drawer from "@suid/material/Drawer" import IconButton from "@suid/material/IconButton" import Toolbar from "@suid/material/Toolbar" @@ -916,7 +915,7 @@ const InstanceShell2: Component = (props) => { showFooter={false} /> - +
{(activeSession) => ( <> diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index dbd22428..39638e1b 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -1,5 +1,5 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js" -import { renderMarkdown, onLanguagesLoaded, decodeHtmlEntities } from "../lib/markdown" +import { renderMarkdown, onLanguagesLoaded, decodeHtmlEntities, setMarkdownTheme } from "../lib/markdown" import { useGlobalCache } from "../lib/hooks/use-global-cache" import type { TextPart, RenderCache } from "../types/message" import { getLogger } from "../lib/logger" @@ -72,6 +72,9 @@ export function Markdown(props: MarkdownProps) { createEffect(async () => { const { part, text, themeKey, highlightEnabled, version } = resolved() + // Ensure the markdown highlighter theme matches the active UI theme. + setMarkdownTheme(themeKey === "dark") + latestRequestedText = text const cacheMatches = (cache: RenderCache | undefined) => { @@ -171,6 +174,8 @@ export function Markdown(props: MarkdownProps) { const { part, text, themeKey, version } = resolved() + setMarkdownTheme(themeKey === "dark") + if (latestRequestedText !== text) { return } diff --git a/packages/ui/src/lib/markdown.ts b/packages/ui/src/lib/markdown.ts index a8287d15..400d7476 100644 --- a/packages/ui/src/lib/markdown.ts +++ b/packages/ui/src/lib/markdown.ts @@ -329,6 +329,10 @@ export async function initMarkdown(isDark: boolean) { isInitialized = true } +export function setMarkdownTheme(isDark: boolean) { + currentTheme = isDark ? "dark" : "light" +} + export function isMarkdownReady(): boolean { return isInitialized && highlighter !== null } diff --git a/packages/ui/src/styles/messaging/message-base.css b/packages/ui/src/styles/messaging/message-base.css index eccab659..5f329d19 100644 --- a/packages/ui/src/styles/messaging/message-base.css +++ b/packages/ui/src/styles/messaging/message-base.css @@ -175,7 +175,8 @@ .message-reasoning { @apply my-2 border rounded; - border-color: var(--border-base); + --reasoning-border-color: color-mix(in oklab, var(--border-base) 62%, var(--text-primary)); + border-color: var(--reasoning-border-color); background-color: var(--surface-secondary); color: inherit; } @@ -286,6 +287,7 @@ } .message-reasoning-card { + --reasoning-border-color: color-mix(in oklab, var(--border-base) 62%, var(--text-primary)); background-color: var(--message-assistant-bg); border-left: 4px solid var(--message-assistant-border); margin-top: 0; @@ -339,7 +341,7 @@ justify-content: center; height: 1.5rem; padding: 0 0.75rem; - border: 1px solid var(--border-base); + border: 1px solid var(--reasoning-border-color, var(--border-base)); border-radius: 0.375rem; background-color: transparent; color: var(--text-muted); @@ -381,6 +383,7 @@ @apply flex flex-col; margin: 0; padding: 0.75rem; + border: 1px solid var(--reasoning-border-color, var(--border-base)); max-height: 30rem; overflow-y: auto; scrollbar-width: thin; @@ -397,4 +400,3 @@ white-space: pre-wrap; margin: 0; } - diff --git a/packages/ui/src/styles/messaging/message-timeline.css b/packages/ui/src/styles/messaging/message-timeline.css index 9fe5bc65..391bbc2e 100644 --- a/packages/ui/src/styles/messaging/message-timeline.css +++ b/packages/ui/src/styles/messaging/message-timeline.css @@ -103,7 +103,7 @@ } .message-timeline-segment-active { - border-color: transparent; + border-color: color-mix(in oklab, var(--timeline-segment-active-bg) 92%, var(--timeline-segment-active-text)); background-color: var(--timeline-segment-active-bg); color: var(--timeline-segment-active-text); font-weight: 700; @@ -182,7 +182,7 @@ .message-timeline-segment-active { background-color: var(--timeline-segment-active-bg) !important; - border-color: transparent !important; + border-color: color-mix(in oklab, var(--timeline-segment-active-bg) 92%, var(--timeline-segment-active-text)) !important; color: var(--timeline-segment-active-text) !important; font-weight: 700; box-shadow: var(--timeline-segment-active-ring); diff --git a/packages/ui/src/styles/messaging/prompt-input.css b/packages/ui/src/styles/messaging/prompt-input.css index b5507031..82f051e3 100644 --- a/packages/ui/src/styles/messaging/prompt-input.css +++ b/packages/ui/src/styles/messaging/prompt-input.css @@ -38,7 +38,8 @@ @apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors; font-family: inherit; background-color: var(--surface-base); - color: inherit; + color: var(--text-primary); + caret-color: var(--text-primary); border-color: var(--border-base); line-height: var(--line-height-normal); border-radius: 0; diff --git a/packages/ui/src/styles/messaging/tool-call.css b/packages/ui/src/styles/messaging/tool-call.css index f39f470a..38bfd1f4 100644 --- a/packages/ui/src/styles/messaging/tool-call.css +++ b/packages/ui/src/styles/messaging/tool-call.css @@ -21,7 +21,7 @@ .tool-call-header-button { background-color: transparent; - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color, var(--border-base)); color: var(--text-secondary); padding: 0.15rem 0.75rem; border-radius: 0.375rem; @@ -58,7 +58,7 @@ font-family: var(--font-family-mono); color: inherit; background-color: var(--surface-secondary); - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color, var(--border-base)); padding: 2px 6px; border-radius: 3px; font-size: 13px; @@ -66,7 +66,9 @@ .tool-call { @apply border overflow-hidden; - border-color: var(--border-base); + /* Tool-call output borders need more contrast in light mode. */ + --tool-call-border-color: color-mix(in oklab, var(--border-base) 62%, var(--text-primary)); + border-color: var(--tool-call-border-color); color: inherit; --tool-call-line-unit: 1.4em; --tool-call-lines-compact: 15; @@ -86,13 +88,14 @@ font-family: var(--font-family-mono); font-size: 13px; border-radius: 0; + color: var(--text-primary); } .tool-call-header::before { content: "▶"; font-size: 11px; margin-right: 0.35rem; - color: var(--text-muted); + color: var(--text-secondary); } .tool-call-header[aria-expanded="true"]::before { @@ -115,6 +118,7 @@ .tool-call-summary { @apply flex-1 text-left inline-flex items-center gap-2; + color: var(--text-primary); } .tool-call-summary::before { @@ -130,6 +134,8 @@ margin-right: 0.35rem; } +/* ToolState uses status="completed"; keep "success" as a legacy alias. */ +.tool-call-status-completed, .tool-call-status-success { border-left: 3px solid var(--status-success); } @@ -157,7 +163,7 @@ .tool-call-preview { @apply p-2 flex flex-col gap-1.5; background-color: var(--surface-code); - border-top: 1px solid var(--border-base); + border-top: 1px solid var(--tool-call-border-color); } .tool-call-preview-label { @@ -186,7 +192,8 @@ .tool-call-markdown { background-color: var(--surface-code); - border: none; + /* Keep a visible frame around the scroll viewport (not the content). */ + border: 1px solid var(--tool-call-border-color); border-radius: 0; padding: 0; font-size: var(--font-size-xs); @@ -199,6 +206,16 @@ position: relative; } +/* Inner code blocks should not own the frame border; the scroll container does. */ +.tool-call-markdown .markdown-code-block { + border: none; +} + +/* Avoid double borders when ANSI output uses .tool-call-content inside tool-call-markdown. */ +.tool-call-markdown .tool-call-content { + border: none; +} + .tool-call-markdown-large { max-height: var(--tool-call-max-height-large, calc(48 * 1.4em)); } @@ -216,7 +233,7 @@ .tool-call-diff-toolbar { @apply flex items-center justify-between gap-3 px-3 py-2; background-color: var(--surface-secondary); - border-bottom: 1px solid var(--border-base); + border-bottom: 1px solid var(--tool-call-border-color); position: sticky; top: 0; z-index: 2; @@ -242,7 +259,7 @@ .tool-call-diff-mode-button { @apply border text-xs font-semibold px-3 py-1 rounded transition-all duration-150; - border-color: var(--border-base); + border-color: var(--tool-call-border-color, var(--border-base)); background-color: transparent; color: var(--text-secondary); } @@ -303,7 +320,7 @@ font-size: 12px; padding: 2px 6px; border-radius: 0.375rem; - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color, var(--border-base)); background-color: var(--surface-code); } @@ -312,7 +329,7 @@ font-size: 13px; color: var(--text-primary); background-color: var(--surface-code); - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color, var(--border-base)); border-radius: 0.5rem; padding: 0.5rem 0.75rem; word-break: break-word; @@ -395,12 +412,6 @@ .tool-call-markdown .markdown-code-block { margin: 0; border-radius: 0; -} - - -.tool-call-markdown .markdown-code-block { - margin: 0; - border: none; background-color: transparent; } @@ -408,7 +419,25 @@ position: sticky; top: 0; z-index: auto; - box-shadow: 0 1px 0 var(--border-base); + background-color: color-mix(in oklab, var(--surface-secondary) 70%, var(--text-primary)); + border-bottom: 1px solid var(--tool-call-border-color); + box-shadow: none; +} + +/* Tool output header (language + copy) needs stronger contrast in light mode. */ +.tool-call-markdown .code-block-language { + color: var(--text-primary); +} + +.tool-call-markdown .code-block-copy { + border-color: var(--tool-call-border-color); + color: var(--text-primary); +} + +/* Plain (non-highlighted) tool output must remain readable in light mode. */ +.tool-call-markdown .markdown-code-block pre:not(.shiki), +.tool-call-markdown .markdown-code-block pre:not(.shiki) code { + color: var(--text-primary); } .tool-call-markdown .markdown-code-block pre { @@ -458,12 +487,12 @@ flex-direction: column; gap: var(--space-xs); padding: var(--space-sm) var(--space-md); - border-top: 1px solid var(--border-base); + border-top: 1px solid var(--tool-call-border-color); background-color: var(--surface-base); } .tool-call-diagnostics-wrapper { - border-top: 1px solid var(--border-base); + border-top: 1px solid var(--tool-call-border-color); background-color: var(--surface-base); margin-top: var(--space-md); } @@ -515,7 +544,7 @@ width: 1.25rem; height: 1.25rem; border-radius: var(--radius-sm); - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color, var(--border-base)); font-size: 12px; } @@ -616,7 +645,7 @@ margin: 0; padding: 8px; background-color: var(--surface-code); - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color); border-radius: 0px; overflow-x: auto; max-height: var(--tool-call-max-height-compact, calc(25 * 1.4em)); @@ -659,7 +688,7 @@ .tool-call-action-button { @apply border text-xs font-semibold px-3 py-1 rounded transition-colors h-8 flex items-center; - border-color: var(--border-base); + border-color: var(--tool-call-border-color, var(--border-base)); color: var(--text-secondary); background-color: transparent; } @@ -681,7 +710,7 @@ .tool-call-content { background-color: var(--surface-code); - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color); border-radius: 0; padding: 8px 12px; font-family: var(--font-family-mono); diff --git a/packages/ui/src/styles/messaging/tool-call/task.css b/packages/ui/src/styles/messaging/tool-call/task.css index 2756cca2..a98d6500 100644 --- a/packages/ui/src/styles/messaging/tool-call/task.css +++ b/packages/ui/src/styles/messaging/tool-call/task.css @@ -6,7 +6,7 @@ } .tool-call-task-section { - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color, var(--border-base)); overflow: hidden; background-color: transparent; border-radius: 0; @@ -19,7 +19,7 @@ gap: 0.75rem; padding: 0.5rem; background-color: var(--surface-secondary); - border-bottom: 1px solid var(--border-base); + border-bottom: 1px solid var(--tool-call-border-color, var(--border-base)); font-family: var(--font-family-mono); font-size: 13px; color: inherit; @@ -81,7 +81,7 @@ align-items: center; gap: 0.4rem; padding: 0.35rem 0.5rem 0.35rem 0.75rem; - border-left: 2px solid var(--border-base); + border-left: 2px solid var(--tool-call-border-color, var(--border-base)); font-size: var(--font-size-sm); font-family: var(--font-family-mono); line-height: 1.35; diff --git a/packages/ui/src/styles/messaging/tool-call/todo.css b/packages/ui/src/styles/messaging/tool-call/todo.css index bc25d0e0..50071667 100644 --- a/packages/ui/src/styles/messaging/tool-call/todo.css +++ b/packages/ui/src/styles/messaging/tool-call/todo.css @@ -16,7 +16,7 @@ .tool-call-todo-item { @apply flex items-start gap-3; - border: 1px solid var(--border-base); + border: 1px solid var(--tool-call-border-color, var(--border-base)); border-radius: 0; padding: 10px 12px; background-color: var(--surface-secondary); @@ -40,7 +40,7 @@ width: 1.1rem; height: 1.1rem; border-radius: 9999px; - border: 2px solid var(--border-base); + border: 2px solid var(--tool-call-border-color, var(--border-base)); display: inline-flex; align-items: center; justify-content: center; diff --git a/packages/ui/src/styles/panels/tabs.css b/packages/ui/src/styles/panels/tabs.css index 98bae37e..50f3cb8a 100644 --- a/packages/ui/src/styles/panels/tabs.css +++ b/packages/ui/src/styles/panels/tabs.css @@ -87,6 +87,23 @@ ring-offset-color: inherit; } +/* Instance tabs: session-status dots should be lighter/softer than list/status pills. */ +.tab-base .status-indicator.session-status.session-working { + --session-status-dot: color-mix(in oklab, var(--session-status-working-fg) 55%, var(--surface-base)); +} + +.tab-base .status-indicator.session-status.session-compacting { + --session-status-dot: color-mix(in oklab, var(--session-status-compacting-fg) 55%, var(--surface-base)); +} + +.tab-base .status-indicator.session-status.session-idle { + --session-status-dot: color-mix(in oklab, var(--session-status-idle-fg) 55%, var(--surface-base)); +} + +.tab-base .status-indicator.session-status.session-permission { + --session-status-dot: color-mix(in oklab, var(--session-status-permission-fg) 55%, var(--surface-base)); +} + .new-tab-button { @apply inline-flex items-center justify-center w-8 h-8 rounded-md transition-colors; background-color: var(--new-tab-bg); diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index 4f8763df..19edbf6a 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -57,6 +57,7 @@ --attachment-chip-ring: rgba(0, 102, 255, 0.1); --badge-neutral-bg: rgba(0, 102, 255, 0.05); --badge-neutral-text: #0066ff; + --badge-success-bg: rgba(76, 175, 80, 0.12); --status-ready-fg: #16a34a; --status-ready-bg: rgba(34, 197, 94, 0.1); --status-starting-fg: #ca8a04; @@ -94,9 +95,10 @@ --status-success-ring: color-mix(in oklab, var(--status-success) 45%, transparent); --border-critical: var(--status-error); - --timeline-segment-active-bg: #0f5b44; - --timeline-segment-active-text: #ffffff; - --timeline-segment-active-ring: inset 0 0 0 1px rgba(0, 0, 0, 0.22); + /* Message timeline active segment (light theme should be a light tint). */ + --timeline-segment-active-bg: #b7e6d6; + --timeline-segment-active-text: #032f23; + --timeline-segment-active-ring: inset 0 0 0 1px rgba(3, 47, 35, 0.28); --button-danger-bg: color-mix(in oklab, var(--status-error) 85%, var(--surface-base)); --button-danger-hover-bg: color-mix(in oklab, var(--status-error) 90%, var(--surface-base)); @@ -223,6 +225,7 @@ --attachment-chip-ring: rgba(0, 128, 255, 0.2); --badge-neutral-bg: rgba(0, 128, 255, 0.15); --badge-neutral-text: #0080ff; + --badge-success-bg: rgba(76, 175, 80, 0.22); --status-ready-fg: #22c55e; --status-ready-bg: rgba(34, 197, 94, 0.2); --status-starting-fg: #facc15; @@ -385,6 +388,7 @@ --attachment-chip-ring: rgba(0, 128, 255, 0.2); --badge-neutral-bg: rgba(0, 128, 255, 0.15); --badge-neutral-text: #0080ff; + --badge-success-bg: rgba(76, 175, 80, 0.22); --status-ready-fg: #22c55e; --status-ready-bg: rgba(34, 197, 94, 0.2); --status-starting-fg: #facc15; From d2b68159bed5bafe82714fc50ef667b1bd85e46a Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 17:37:02 +0000 Subject: [PATCH 09/14] chore(opencode-config): bump @opencode-ai/plugin --- packages/opencode-config/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode-config/package.json b/packages/opencode-config/package.json index edb8a6ae..eb4ef9f4 100644 --- a/packages/opencode-config/package.json +++ b/packages/opencode-config/package.json @@ -4,6 +4,6 @@ "private": true, "license": "MIT", "dependencies": { - "@opencode-ai/plugin": "1.1.36" + "@opencode-ai/plugin": "1.1.42" } } From 0261154a5eaae4933af23ac717063af75731c4d3 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 18:32:54 +0000 Subject: [PATCH 10/14] feat(ui): add delete action for message parts --- packages/ui/src/components/message-block.tsx | 178 ++++++++++++++++-- packages/ui/src/components/message-item.tsx | 107 ++++++++++- packages/ui/src/components/message-part.tsx | 31 +-- .../ui/src/lib/i18n/messages/en/messaging.ts | 10 + .../ui/src/lib/i18n/messages/es/messaging.ts | 10 + .../ui/src/lib/i18n/messages/fr/messaging.ts | 10 + .../ui/src/lib/i18n/messages/ja/messaging.ts | 10 + .../ui/src/lib/i18n/messages/ru/messaging.ts | 10 + .../lib/i18n/messages/zh-Hans/messaging.ts | 10 + packages/ui/src/stores/session-actions.ts | 23 +++ 10 files changed, 363 insertions(+), 36 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index a9a01eea..0a2de252 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -11,6 +11,8 @@ import { messageStoreBus } from "../stores/message-v2/bus" import { formatTokenTotal } from "../lib/formatters" import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions" import { setActiveInstanceId } from "../stores/instances" +import { showAlertDialog } from "../stores/alerts" +import { deleteMessagePart } from "../stores/session-actions" import { useI18n } from "../lib/i18n" const TOOL_ICON = "🔧" @@ -302,6 +304,7 @@ interface ToolCallItemProps { function ToolCallItem(props: ToolCallItemProps) { const { t } = useI18n() + const [deleting, setDeleting] = createSignal(false) const record = createMemo(() => props.store().getMessage(props.messageId)) const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId)) @@ -318,6 +321,14 @@ function ToolCallItem(props: ToolCallItemProps) { const messageVersion = createMemo(() => record()?.revision ?? 0) const partVersion = createMemo(() => partEntry()?.revision ?? 0) + const deleteDisabled = createMemo(() => { + if (deleting()) return true + // Avoid deleting while a tool is actively running to prevent confusing UI states. + if (isToolStateRunning(toolState())) return true + // Avoid deleting permission prompts from here; those are interactive. + return Boolean(toolPart()?.pendingPermission) + }) + const taskSessionId = createMemo(() => { const state = toolState() if (!state) return "" @@ -341,6 +352,26 @@ function ToolCallItem(props: ToolCallItemProps) { navigateToTaskSession(location) } + const handleDeleteToolPart = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + + if (deleteDisabled()) return + + setDeleting(true) + try { + await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId) + } catch (error) { + showAlertDialog(t("messageBlock.tool.deletePart.failed.message"), { + title: t("messageBlock.tool.deletePart.failed.title"), + detail: error instanceof Error ? error.message : String(error), + variant: "error", + }) + } finally { + setDeleting(false) + } + } + return ( {(resolvedToolPart) => ( @@ -351,17 +382,30 @@ function ToolCallItem(props: ToolCallItemProps) { {t("messageBlock.tool.header")} {toolName() || t("messageBlock.tool.unknown")}
- + +
+ + + + - +
- + - + @@ -689,8 +758,19 @@ interface StepCardProps { borderColor?: string } -function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; borderColor?: string }) { +interface CompactionCardProps { + part: ClientPart + messageInfo?: MessageInfo + borderColor?: string + instanceId: string + sessionId: string + messageId: string + partId: string +} + +function CompactionCard(props: CompactionCardProps) { const { t } = useI18n() + const [deleting, setDeleting] = createSignal(false) const isAuto = () => Boolean((props.part as any)?.auto) const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel")) const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR) @@ -698,13 +778,43 @@ function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; bo const containerClass = () => `message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}` + const canDelete = () => Boolean(props.partId) && !deleting() + + const handleDelete = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + if (!canDelete()) return + setDeleting(true) + try { + await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId) + } catch (error) { + showAlertDialog(t("messagePart.actions.deleteFailedMessage"), { + title: t("messagePart.actions.deleteFailedTitle"), + detail: error instanceof Error ? error.message : String(error), + variant: "error", + }) + } finally { + setDeleting(false) + } + } + return (
+ +
@@ -337,6 +413,19 @@ export default function MessageItem(props: MessageItemProps) { + +
{name} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 1256d353..3f8405d4 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -15,7 +15,7 @@ interface MessagePartProps { sessionId: string onRendered?: () => void } - export default function MessagePart(props: MessagePartProps) { + export default function MessagePart(props: MessagePartProps) { const { isDark } = useTheme() const { preferences } = useConfig() @@ -32,6 +32,7 @@ interface MessagePartProps { return Boolean((part as any).synthetic) && props.messageType !== "user" } + const plainTextContent = () => { const part = props.part @@ -103,21 +104,21 @@ interface MessagePartProps {
- {plainTextContent()}} - > - - + {plainTextContent()}} + > + + -
+
diff --git a/packages/ui/src/lib/i18n/messages/en/messaging.ts b/packages/ui/src/lib/i18n/messages/en/messaging.ts index 1da24bb1..b2c8568a 100644 --- a/packages/ui/src/lib/i18n/messages/en/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/en/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Go to Session", "messageBlock.tool.goToSession.title": "Go to session", "messageBlock.tool.goToSession.unavailableTitle": "Session not available yet", + "messageBlock.tool.deletePart.label": "Delete", + "messageBlock.tool.deletePart.deleting": "Deleting...", + "messageBlock.tool.deletePart.title": "Delete this tool call output", + "messageBlock.tool.deletePart.failed.title": "Delete failed", + "messageBlock.tool.deletePart.failed.message": "Failed to delete tool call output", "messageBlock.compaction.ariaLabel": "Session compaction", "messageBlock.compaction.autoLabel": "Session auto-compacted", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "Generating...", "messageItem.status.sending": "Sending...", "messageItem.status.failedToSend": "Message failed to send", + "messagePart.actions.delete": "Delete", + "messagePart.actions.deleting": "Deleting...", + "messagePart.actions.deleteTitle": "Delete this item", + "messagePart.actions.deleteFailedTitle": "Delete failed", + "messagePart.actions.deleteFailedMessage": "Failed to delete item", "messageItem.attachment.defaultName": "attachment", "messageItem.attachment.downloadAriaLabel": "Download {name}", "messageItem.agentMeta.agentLabel": "Agent: {agent}", diff --git a/packages/ui/src/lib/i18n/messages/es/messaging.ts b/packages/ui/src/lib/i18n/messages/es/messaging.ts index 5cca187a..1c1da653 100644 --- a/packages/ui/src/lib/i18n/messages/es/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/es/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Ir a sesión", "messageBlock.tool.goToSession.title": "Ir a la sesión", "messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible", + "messageBlock.tool.deletePart.label": "Eliminar", + "messageBlock.tool.deletePart.deleting": "Eliminando...", + "messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta", + "messageBlock.tool.deletePart.failed.title": "Error al eliminar", + "messageBlock.tool.deletePart.failed.message": "No se pudo eliminar la salida de herramienta", "messageBlock.compaction.ariaLabel": "Compactación de sesión", "messageBlock.compaction.autoLabel": "Sesión compactada automáticamente", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "Generando...", "messageItem.status.sending": "Enviando...", "messageItem.status.failedToSend": "No se pudo enviar el mensaje", + "messagePart.actions.delete": "Eliminar", + "messagePart.actions.deleting": "Eliminando...", + "messagePart.actions.deleteTitle": "Eliminar este elemento", + "messagePart.actions.deleteFailedTitle": "Error al eliminar", + "messagePart.actions.deleteFailedMessage": "No se pudo eliminar el elemento", "messageItem.attachment.defaultName": "adjunto", "messageItem.attachment.downloadAriaLabel": "Descargar {name}", "messageItem.agentMeta.agentLabel": "Agente: {agent}", diff --git a/packages/ui/src/lib/i18n/messages/fr/messaging.ts b/packages/ui/src/lib/i18n/messages/fr/messaging.ts index 0e786925..ef8fecf1 100644 --- a/packages/ui/src/lib/i18n/messages/fr/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/fr/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Aller à la session", "messageBlock.tool.goToSession.title": "Aller à la session", "messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible", + "messageBlock.tool.deletePart.label": "Supprimer", + "messageBlock.tool.deletePart.deleting": "Suppression...", + "messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil", + "messageBlock.tool.deletePart.failed.title": "Échec de suppression", + "messageBlock.tool.deletePart.failed.message": "Impossible de supprimer la sortie d'outil", "messageBlock.compaction.ariaLabel": "Compaction de la session", "messageBlock.compaction.autoLabel": "Session compactée automatiquement", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "Génération...", "messageItem.status.sending": "Envoi...", "messageItem.status.failedToSend": "Échec de l'envoi du message", + "messagePart.actions.delete": "Supprimer", + "messagePart.actions.deleting": "Suppression...", + "messagePart.actions.deleteTitle": "Supprimer cet élément", + "messagePart.actions.deleteFailedTitle": "Échec de suppression", + "messagePart.actions.deleteFailedMessage": "Impossible de supprimer l'élément", "messageItem.attachment.defaultName": "piece-jointe", "messageItem.attachment.downloadAriaLabel": "Télécharger {name}", "messageItem.agentMeta.agentLabel": "Agent : {agent}", diff --git a/packages/ui/src/lib/i18n/messages/ja/messaging.ts b/packages/ui/src/lib/i18n/messages/ja/messaging.ts index f9aa26ed..25823eb9 100644 --- a/packages/ui/src/lib/i18n/messages/ja/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ja/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "セッションへ移動", "messageBlock.tool.goToSession.title": "セッションへ移動", "messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません", + "messageBlock.tool.deletePart.label": "削除", + "messageBlock.tool.deletePart.deleting": "削除中...", + "messageBlock.tool.deletePart.title": "このツール出力を削除", + "messageBlock.tool.deletePart.failed.title": "削除に失敗しました", + "messageBlock.tool.deletePart.failed.message": "ツール出力の削除に失敗しました", "messageBlock.compaction.ariaLabel": "セッションのコンパクト化", "messageBlock.compaction.autoLabel": "セッションを自動でコンパクト化しました", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "生成中...", "messageItem.status.sending": "送信中...", "messageItem.status.failedToSend": "メッセージの送信に失敗しました", + "messagePart.actions.delete": "削除", + "messagePart.actions.deleting": "削除中...", + "messagePart.actions.deleteTitle": "この項目を削除", + "messagePart.actions.deleteFailedTitle": "削除に失敗しました", + "messagePart.actions.deleteFailedMessage": "項目の削除に失敗しました", "messageItem.attachment.defaultName": "添付ファイル", "messageItem.attachment.downloadAriaLabel": "{name} をダウンロード", "messageItem.agentMeta.agentLabel": "エージェント: {agent}", diff --git a/packages/ui/src/lib/i18n/messages/ru/messaging.ts b/packages/ui/src/lib/i18n/messages/ru/messaging.ts index eeabe2f9..3fe9e15d 100644 --- a/packages/ui/src/lib/i18n/messages/ru/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ru/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Перейти к сессии", "messageBlock.tool.goToSession.title": "Перейти к сессии", "messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна", + "messageBlock.tool.deletePart.label": "Удалить", + "messageBlock.tool.deletePart.deleting": "Удаление...", + "messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента", + "messageBlock.tool.deletePart.failed.title": "Ошибка удаления", + "messageBlock.tool.deletePart.failed.message": "Не удалось удалить вывод инструмента", "messageBlock.compaction.ariaLabel": "Компактация сессии", "messageBlock.compaction.autoLabel": "Сессия автоматически компактирована", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "Генерация…", "messageItem.status.sending": "Отправка…", "messageItem.status.failedToSend": "Не удалось отправить сообщение", + "messagePart.actions.delete": "Удалить", + "messagePart.actions.deleting": "Удаление...", + "messagePart.actions.deleteTitle": "Удалить этот элемент", + "messagePart.actions.deleteFailedTitle": "Ошибка удаления", + "messagePart.actions.deleteFailedMessage": "Не удалось удалить элемент", "messageItem.attachment.defaultName": "вложение", "messageItem.attachment.downloadAriaLabel": "Скачать {name}", "messageItem.agentMeta.agentLabel": "Агент: {agent}", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts index 1946d076..93f7a15d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "前往会话", "messageBlock.tool.goToSession.title": "前往会话", "messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用", + "messageBlock.tool.deletePart.label": "删除", + "messageBlock.tool.deletePart.deleting": "正在删除...", + "messageBlock.tool.deletePart.title": "删除此工具输出", + "messageBlock.tool.deletePart.failed.title": "删除失败", + "messageBlock.tool.deletePart.failed.message": "删除工具输出失败", "messageBlock.compaction.ariaLabel": "会话压缩", "messageBlock.compaction.autoLabel": "会话已自动压缩", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "正在生成...", "messageItem.status.sending": "正在发送...", "messageItem.status.failedToSend": "消息发送失败", + "messagePart.actions.delete": "删除", + "messagePart.actions.deleting": "正在删除...", + "messagePart.actions.deleteTitle": "删除此项", + "messagePart.actions.deleteFailedTitle": "删除失败", + "messagePart.actions.deleteFailedMessage": "删除失败", "messageItem.attachment.defaultName": "附件", "messageItem.attachment.downloadAriaLabel": "下载 {name}", "messageItem.agentMeta.agentLabel": "智能体:{agent}", diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts index 29f21129..ff8e6a2f 100644 --- a/packages/ui/src/stores/session-actions.ts +++ b/packages/ui/src/stores/session-actions.ts @@ -6,6 +6,7 @@ import { providers, sessions, withSession } from "./session-state" import { getDefaultModel, isModelValid } from "./session-models" import { updateSessionInfo } from "./message-v2/session-info" import { messageStoreBus } from "./message-v2/bus" +import { removeMessagePartV2 } from "./message-v2/bridge" import { getLogger } from "../lib/logger" import { requestData } from "../lib/opencode-api" @@ -395,8 +396,30 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s }) } +async function deleteMessagePart(instanceId: string, sessionId: string, messageId: string, partId: string): Promise { + if (!instanceId || !sessionId || !messageId || !partId) return + const instance = instances().get(instanceId) + if (!instance || !instance.client) { + throw new Error("Instance not ready") + } + + await requestData( + instance.client.part.delete({ + sessionID: sessionId, + messageID: messageId, + partID: partId, + }), + "part.delete", + ) + + // Optimistic removal; SSE will also broadcast a part-removed event. + removeMessagePartV2(instanceId, messageId, partId) + updateSessionInfo(instanceId, sessionId) +} + export { abortSession, + deleteMessagePart, executeCustomCommand, renameSession, runShellCommand, From 02407e0f7ab044d6fd19600c644cc6ea2e18e9af Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 19:02:47 +0000 Subject: [PATCH 11/14] fix(ui): restore dark tab and tool output styling Use tokenized border contrast so dark mode borders stay subtle, keep instance tab status dots vivid in dark themes, and adjust tool-call code block header background via a dedicated token. --- .../ui/src/styles/messaging/message-base.css | 4 +-- .../ui/src/styles/messaging/tool-call.css | 14 ++++++-- packages/ui/src/styles/panels/tabs.css | 32 +++++++++++++++++++ packages/ui/src/styles/tokens.css | 18 +++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/styles/messaging/message-base.css b/packages/ui/src/styles/messaging/message-base.css index 5f329d19..13b3551c 100644 --- a/packages/ui/src/styles/messaging/message-base.css +++ b/packages/ui/src/styles/messaging/message-base.css @@ -175,7 +175,7 @@ .message-reasoning { @apply my-2 border rounded; - --reasoning-border-color: color-mix(in oklab, var(--border-base) 62%, var(--text-primary)); + --reasoning-border-color: var(--border-strong, var(--border-base)); border-color: var(--reasoning-border-color); background-color: var(--surface-secondary); color: inherit; @@ -287,7 +287,7 @@ } .message-reasoning-card { - --reasoning-border-color: color-mix(in oklab, var(--border-base) 62%, var(--text-primary)); + --reasoning-border-color: var(--border-strong, var(--border-base)); background-color: var(--message-assistant-bg); border-left: 4px solid var(--message-assistant-border); margin-top: 0; diff --git a/packages/ui/src/styles/messaging/tool-call.css b/packages/ui/src/styles/messaging/tool-call.css index 38bfd1f4..6270e536 100644 --- a/packages/ui/src/styles/messaging/tool-call.css +++ b/packages/ui/src/styles/messaging/tool-call.css @@ -66,8 +66,8 @@ .tool-call { @apply border overflow-hidden; - /* Tool-call output borders need more contrast in light mode. */ - --tool-call-border-color: color-mix(in oklab, var(--border-base) 62%, var(--text-primary)); + /* Tokenized so dark mode doesn't get overly bright borders. */ + --tool-call-border-color: var(--border-strong, var(--border-base)); border-color: var(--tool-call-border-color); color: inherit; --tool-call-line-unit: 1.4em; @@ -419,7 +419,7 @@ position: sticky; top: 0; z-index: auto; - background-color: color-mix(in oklab, var(--surface-secondary) 70%, var(--text-primary)); + background-color: var(--code-block-header-bg, var(--surface-secondary)); border-bottom: 1px solid var(--tool-call-border-color); box-shadow: none; } @@ -447,6 +447,14 @@ overflow-y: visible; } +/* Shiki injects inline background colors; force token surfaces. */ +.tool-call-markdown pre.shiki, +.tool-call-markdown pre.shiki code, +.tool-call-markdown .shiki { + background: transparent !important; + background-color: transparent !important; +} + .tool-call-markdown::-webkit-scrollbar { width: 8px; } diff --git a/packages/ui/src/styles/panels/tabs.css b/packages/ui/src/styles/panels/tabs.css index 50f3cb8a..76d37ce1 100644 --- a/packages/ui/src/styles/panels/tabs.css +++ b/packages/ui/src/styles/panels/tabs.css @@ -104,6 +104,38 @@ --session-status-dot: color-mix(in oklab, var(--session-status-permission-fg) 55%, var(--surface-base)); } +/* Dark mode: keep dots vivid (avoid muddy mixes). */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) .tab-base .status-indicator.session-status.session-working { + --session-status-dot: var(--session-status-working-fg); + } + :root:not([data-theme]) .tab-base .status-indicator.session-status.session-compacting { + --session-status-dot: var(--session-status-compacting-fg); + } + :root:not([data-theme]) .tab-base .status-indicator.session-status.session-idle { + --session-status-dot: var(--session-status-idle-fg); + } + :root:not([data-theme]) .tab-base .status-indicator.session-status.session-permission { + --session-status-dot: var(--session-status-permission-fg); + } +} + +[data-theme="dark"] .tab-base .status-indicator.session-status.session-working { + --session-status-dot: var(--session-status-working-fg); +} + +[data-theme="dark"] .tab-base .status-indicator.session-status.session-compacting { + --session-status-dot: var(--session-status-compacting-fg); +} + +[data-theme="dark"] .tab-base .status-indicator.session-status.session-idle { + --session-status-dot: var(--session-status-idle-fg); +} + +[data-theme="dark"] .tab-base .status-indicator.session-status.session-permission { + --session-status-dot: var(--session-status-permission-fg); +} + .new-tab-button { @apply inline-flex items-center justify-center w-8 h-8 rounded-md transition-colors; background-color: var(--new-tab-bg); diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index 19edbf6a..794a3843 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -11,6 +11,7 @@ --border-base: #e0e0e0; --border-secondary: #e0e0e0; --border-muted: #e0e0e0; + --border-strong: color-mix(in oklab, var(--border-base) 62%, var(--text-primary)); /* Text tokens */ --text-primary: #111827; @@ -81,6 +82,7 @@ --panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); --panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.18); --popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.18); + --code-block-header-bg: color-mix(in oklab, var(--surface-secondary) 78%, var(--text-primary)); --message-error-bg: rgba(244, 67, 54, 0.1); --message-error-bg-strong: rgba(244, 67, 54, 0.15); --danger-soft-bg: rgba(239, 68, 68, 0.1); @@ -182,6 +184,7 @@ --border-base: #3a3a3a; --border-secondary: #3a3a3a; --border-muted: #3a3a3a; + --border-strong: var(--border-base); /* Text tokens */ --text-primary: #cfd4dc; @@ -248,6 +251,7 @@ --panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); --panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.45); --popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.55); + --code-block-header-bg: var(--surface-secondary); --border-critical: var(--status-error); --timeline-segment-active-bg: #0f5b44; --timeline-segment-active-text: #ffffff; @@ -346,6 +350,7 @@ --border-base: #3a3a3a; --border-secondary: #3a3a3a; --border-muted: #3a3a3a; + --border-strong: var(--border-base); /* Text tokens */ --text-primary: #cfd4dc; @@ -412,6 +417,7 @@ --panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); --panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.45); --popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.55); + --code-block-header-bg: var(--surface-secondary); --border-critical: var(--status-error); --timeline-segment-active-bg: #0f5b44; --timeline-segment-active-text: #ffffff; @@ -430,6 +436,18 @@ --kbd-bg: var(--surface-secondary); --kbd-border: var(--border-base); --kbd-text: var(--text-primary); + --button-primary-bg: #3f3f46; + --button-primary-hover-bg: #52525b; + --button-primary-text: #f5f6f8; + --tab-active-bg: #3f3f46; + --tab-active-hover-bg: #52525b; + --tab-active-text: #f5f6f8; + --tab-inactive-bg: #2a2a31; + --tab-inactive-hover-bg: #3f3f46; + --tab-inactive-text: #d4d4d8; + --new-tab-bg: #3f3f46; + --new-tab-hover-bg: #52525b; + --new-tab-text: #f5f6f8; /* Layout & spacing tokens */ --space-2xs: 2px; From d98d519fd34cbdf4957711990d3f4191ae96a9d9 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 19:42:24 +0000 Subject: [PATCH 12/14] feat(ui): persist theme preference Persist system/light/dark theme mode in app config and default new installs to system so the UI follows OS theme unless overridden. --- packages/ui/src/lib/theme.tsx | 17 ++++------------- packages/ui/src/stores/preferences.tsx | 4 ++-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/lib/theme.tsx b/packages/ui/src/lib/theme.tsx index 654bb772..61ad6e4f 100644 --- a/packages/ui/src/lib/theme.tsx +++ b/packages/ui/src/lib/theme.tsx @@ -78,12 +78,12 @@ const resolvePaletteColors = (dark: boolean): ResolvedPaletteColors => { export function ThemeProvider(props: { children: JSX.Element }) { const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null - const { themePreference } = useConfig() + const { themePreference, setThemePreference } = useConfig() const [isDark, setIsDarkSignal] = createSignal(true) - const [themeMode, setThemeModeSignal] = createSignal(themePreference()) - const [hasUserOverride, setHasUserOverride] = createSignal(false) const [themeRevision, setThemeRevision] = createSignal(0) + const themeMode = () => themePreference() as ThemeMode + const resolveDarkTheme = () => { const mode = themeMode() if (mode === "dark") return true @@ -111,12 +111,6 @@ export function ThemeProvider(props: { children: JSX.Element }) { applyResolvedTheme() }) - createEffect(() => { - const preference = themePreference() - if (hasUserOverride()) return - setThemeModeSignal(preference) - }) - onMount(() => { if (!mediaQuery) return const handleSystemThemeChange = () => { @@ -131,10 +125,7 @@ export function ThemeProvider(props: { children: JSX.Element }) { }) const setThemeMode = (mode: ThemeMode) => { - setHasUserOverride(true) - setThemeModeSignal(mode) - // Persistence is intentionally implemented later. - // When we wire it up, this should call `setThemePreference(mode)`. + setThemePreference(mode) } const cycleThemeMode = () => { diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index 95d623cf..f3213c6e 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -194,7 +194,7 @@ const [isConfigLoaded, setIsConfigLoaded] = createSignal(false) const preferences = createMemo(() => internalConfig().preferences) const recentFolders = createMemo(() => internalConfig().recentFolders ?? []) const opencodeBinaries = createMemo(() => internalConfig().opencodeBinaries ?? []) -const themePreference = createMemo(() => internalConfig().theme ?? "dark") +const themePreference = createMemo(() => internalConfig().theme ?? "system") let loadPromise: Promise | null = null function normalizeConfig(config?: ConfigData | null): ConfigData { @@ -202,7 +202,7 @@ function normalizeConfig(config?: ConfigData | null): ConfigData { preferences: normalizePreferences(config?.preferences), recentFolders: (config?.recentFolders ?? []).map((folder) => ({ ...folder })), opencodeBinaries: (config?.opencodeBinaries ?? []).map((binary) => ({ ...binary })), - theme: config?.theme ?? "dark", + theme: config?.theme ?? "system", } } From 76b1134c95622616e81ab908ccdfbe21613b663a Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 20:12:02 +0000 Subject: [PATCH 13/14] fix(ui): apply theme before initial render --- packages/ui/src/main.tsx | 55 ++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx index 16aa1866..4ab54714 100644 --- a/packages/ui/src/main.tsx +++ b/packages/ui/src/main.tsx @@ -5,6 +5,7 @@ import { ConfigProvider } from "./stores/preferences" import { InstanceConfigProvider } from "./stores/instance-config" import { runtimeEnv } from "./lib/runtime-env" import { I18nProvider } from "./lib/i18n" +import { storage } from "./lib/storage" import "./index.css" import "@git-diff-view/solid/styles/diff-view-pure.css" @@ -14,22 +15,48 @@ if (!root) { throw new Error("Root element not found") } +const mount = root + if (typeof document !== "undefined") { document.documentElement.dataset.runtimeHost = runtimeEnv.host document.documentElement.dataset.runtimePlatform = runtimeEnv.platform } -render( - () => ( - - - - - - - - - - ), - root, -) +async function bootstrap() { + if (typeof document !== "undefined") { + // renderer/index.html currently seeds a dark theme to avoid a white flash. + // Reset to CSS defaults immediately so the first render matches system + // (and then refine once persisted config loads). + document.documentElement.removeAttribute("data-theme") + + try { + const config = await storage.loadConfig() + const theme = config?.theme ?? "system" + + if (theme === "system") { + document.documentElement.removeAttribute("data-theme") + } else { + document.documentElement.setAttribute("data-theme", theme) + } + } catch { + // If config fails to load, fall back to CSS defaults. + } + } + + render( + () => ( + + + + + + + + + + ), + mount, + ) +} + +void bootstrap() From 8a91e04ff99373e4f608bac2003858c69bb0e480 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 20:22:17 +0000 Subject: [PATCH 14/14] Bump to v0.9.4 --- package-lock.json | 12 ++++++------ package.json | 2 +- packages/electron-app/package.json | 2 +- packages/server/package-lock.json | 4 ++-- packages/server/package.json | 2 +- packages/tauri-app/package.json | 2 +- packages/ui/package.json | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 27a2f2ad..cf2f64ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codenomad-workspace", - "version": "0.9.3", + "version": "0.9.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codenomad-workspace", - "version": "0.9.3", + "version": "0.9.4", "license": "MIT", "dependencies": { "7zip-bin": "^5.2.0", @@ -7404,7 +7404,7 @@ }, "packages/electron-app": { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.9.3", + "version": "0.9.4", "license": "MIT", "dependencies": { "@codenomad/ui": "file:../ui", @@ -7439,7 +7439,7 @@ }, "packages/server": { "name": "@neuralnomads/codenomad", - "version": "0.9.3", + "version": "0.9.4", "license": "MIT", "dependencies": { "@fastify/cors": "^8.5.0", @@ -7477,7 +7477,7 @@ }, "packages/tauri-app": { "name": "@codenomad/tauri-app", - "version": "0.9.3", + "version": "0.9.4", "license": "MIT", "devDependencies": { "@tauri-apps/cli": "^2.9.4" @@ -7485,7 +7485,7 @@ }, "packages/ui": { "name": "@codenomad/ui", - "version": "0.9.3", + "version": "0.9.4", "license": "MIT", "dependencies": { "@git-diff-view/solid": "^0.0.8", diff --git a/package.json b/package.json index 96e56dfd..69bd6207 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codenomad-workspace", - "version": "0.9.3", + "version": "0.9.4", "private": true, "description": "CodeNomad monorepo workspace", "license": "MIT", diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index 118d83f6..6be2a5ad 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.9.3", + "version": "0.9.4", "description": "CodeNomad - AI coding assistant", "license": "MIT", "author": { diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json index c8c33586..20182822 100644 --- a/packages/server/package-lock.json +++ b/packages/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@neuralnomads/codenomad", - "version": "0.9.3", + "version": "0.9.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neuralnomads/codenomad", - "version": "0.9.3", + "version": "0.9.4", "dependencies": { "@fastify/cors": "^8.5.0", "@fastify/reply-from": "^9.8.0", diff --git a/packages/server/package.json b/packages/server/package.json index 9d9d17f3..5a6fbbe9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad", - "version": "0.9.3", + "version": "0.9.4", "description": "CodeNomad Server", "license": "MIT", "author": { diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json index 1af212df..4caf705b 100644 --- a/packages/tauri-app/package.json +++ b/packages/tauri-app/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/tauri-app", - "version": "0.9.3", + "version": "0.9.4", "private": true, "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index e5847147..27f833a7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/ui", - "version": "0.9.3", + "version": "0.9.4", "private": true, "license": "MIT", "type": "module",