Disable macOS spellchecker and simplify reactive lists
This commit is contained in:
@@ -4,6 +4,8 @@ import { createApplicationMenu } from "./menu"
|
|||||||
import { setupInstanceIPC } from "./ipc"
|
import { setupInstanceIPC } from "./ipc"
|
||||||
import { setupStorageIPC } from "./storage"
|
import { setupStorageIPC } from "./storage"
|
||||||
|
|
||||||
|
app.commandLine.appendSwitch("disable-spell-checking")
|
||||||
|
|
||||||
// Setup IPC handlers before creating windows
|
// Setup IPC handlers before creating windows
|
||||||
setupStorageIPC()
|
setupStorageIPC()
|
||||||
|
|
||||||
@@ -27,6 +29,9 @@ function createWindow() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Disable macOS spell server to avoid input lag
|
||||||
|
mainWindow.webContents.session.setSpellCheckerEnabled(false)
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
mainWindow.loadURL("http://localhost:3000")
|
mainWindow.loadURL("http://localhost:3000")
|
||||||
mainWindow.webContents.openDevTools()
|
mainWindow.webContents.openDevTools()
|
||||||
|
|||||||
@@ -18,18 +18,15 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
|||||||
<div class="tab-bar tab-bar-instance">
|
<div class="tab-bar tab-bar-instance">
|
||||||
<div class="tab-container" role="tablist">
|
<div class="tab-container" role="tablist">
|
||||||
<div class="flex items-center gap-1 overflow-x-auto">
|
<div class="flex items-center gap-1 overflow-x-auto">
|
||||||
<For each={Array.from(props.instances.keys())}>
|
<For each={Array.from(props.instances.entries())}>
|
||||||
{(id) => {
|
{([id, instance]) => (
|
||||||
const instance = props.instances.get(id)
|
<InstanceTab
|
||||||
return (
|
instance={instance}
|
||||||
<InstanceTab
|
active={id === props.activeInstanceId}
|
||||||
instance={instance!}
|
onSelect={() => props.onSelect(id)}
|
||||||
active={id === props.activeInstanceId}
|
onClose={() => props.onClose(id)}
|
||||||
onSelect={() => props.onSelect(id)}
|
/>
|
||||||
onClose={() => props.onClose(id)}
|
)}
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
<button
|
<button
|
||||||
class="new-tab-button"
|
class="new-tab-button"
|
||||||
@@ -40,7 +37,7 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
|||||||
<Plus class="w-4 h-4" />
|
<Plus class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.instances.size > 1}>
|
<Show when={Array.from(props.instances.entries()).length > 1}>
|
||||||
<div class="flex-shrink-0 ml-4">
|
<div class="flex-shrink-0 ml-4">
|
||||||
<KeyboardHint
|
<KeyboardHint
|
||||||
shortcuts={[keyboardRegistry.get("instance-prev")!, keyboardRegistry.get("instance-next")!].filter(
|
shortcuts={[keyboardRegistry.get("instance-prev")!, keyboardRegistry.get("instance-next")!].filter(
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ const LogsView: Component<LogsViewProps> = (props) => {
|
|||||||
const instance = () => instances().get(props.instanceId)
|
const instance = () => instances().get(props.instanceId)
|
||||||
const logs = () => instance()?.logs ?? []
|
const logs = () => instance()?.logs ?? []
|
||||||
|
|
||||||
let renderedCount = 0
|
|
||||||
let initialSyncDone = false
|
|
||||||
let emptyStateEl: HTMLDivElement | null = null
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (scrollRef && savedState) {
|
if (scrollRef && savedState) {
|
||||||
scrollRef.scrollTop = savedState.scrollTop
|
scrollRef.scrollTop = savedState.scrollTop
|
||||||
@@ -36,6 +32,11 @@ const LogsView: Component<LogsViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (autoScroll() && scrollRef && logs().length > 0) {
|
||||||
|
scrollRef.scrollTop = scrollRef.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!scrollRef) return
|
if (!scrollRef) return
|
||||||
@@ -75,78 +76,6 @@ const LogsView: Component<LogsViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createLogElement = (entry: LogEntry) => {
|
|
||||||
const row = document.createElement("div")
|
|
||||||
row.className = "log-entry"
|
|
||||||
|
|
||||||
const timestamp = document.createElement("span")
|
|
||||||
timestamp.className = "log-timestamp"
|
|
||||||
timestamp.textContent = formatTime(entry.timestamp)
|
|
||||||
|
|
||||||
const message = document.createElement("span")
|
|
||||||
message.className = `log-message ${getLevelColor(entry.level)}`
|
|
||||||
message.textContent = entry.message
|
|
||||||
|
|
||||||
row.append(timestamp, message)
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const entries = logs()
|
|
||||||
if (!scrollRef) return
|
|
||||||
|
|
||||||
if (entries.length < renderedCount) {
|
|
||||||
scrollRef.innerHTML = ""
|
|
||||||
renderedCount = 0
|
|
||||||
initialSyncDone = false
|
|
||||||
if (emptyStateEl && emptyStateEl.parentElement) {
|
|
||||||
emptyStateEl.parentElement.removeChild(emptyStateEl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entries.length === 0) {
|
|
||||||
renderedCount = 0
|
|
||||||
if (!emptyStateEl) {
|
|
||||||
emptyStateEl = document.createElement("div")
|
|
||||||
emptyStateEl.className = "log-empty-state"
|
|
||||||
emptyStateEl.textContent = "Waiting for server output..."
|
|
||||||
}
|
|
||||||
if (emptyStateEl.parentElement !== scrollRef) {
|
|
||||||
scrollRef.appendChild(emptyStateEl)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptyStateEl && emptyStateEl.parentElement === scrollRef) {
|
|
||||||
scrollRef.removeChild(emptyStateEl)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = renderedCount; i < entries.length; i++) {
|
|
||||||
const entry = entries[i]
|
|
||||||
scrollRef.appendChild(createLogElement(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
renderedCount = entries.length
|
|
||||||
|
|
||||||
if (!initialSyncDone) {
|
|
||||||
if (savedState) {
|
|
||||||
const maxScrollTop = Math.max(scrollRef.scrollHeight - scrollRef.clientHeight, 0)
|
|
||||||
const target = Math.min(savedState.scrollTop, maxScrollTop)
|
|
||||||
scrollRef.scrollTop = target
|
|
||||||
setAutoScroll(savedState.autoScroll)
|
|
||||||
} else {
|
|
||||||
scrollRef.scrollTop = scrollRef.scrollHeight
|
|
||||||
setAutoScroll(true)
|
|
||||||
}
|
|
||||||
initialSyncDone = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoScroll()) {
|
|
||||||
scrollRef.scrollTop = scrollRef.scrollHeight
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="log-container">
|
<div class="log-container">
|
||||||
<div class="log-header">
|
<div class="log-header">
|
||||||
@@ -178,7 +107,21 @@ const LogsView: Component<LogsViewProps> = (props) => {
|
|||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
class="log-content"
|
class="log-content"
|
||||||
></div>
|
>
|
||||||
|
<Show
|
||||||
|
when={logs().length > 0}
|
||||||
|
fallback={<div class="log-empty-state">Waiting for server output...</div>}
|
||||||
|
>
|
||||||
|
<For each={logs()}>
|
||||||
|
{(entry) => (
|
||||||
|
<div class="log-entry">
|
||||||
|
<span class="log-timestamp">{formatTime(entry.timestamp)}</span>
|
||||||
|
<span class={`log-message ${getLevelColor(entry.level)}`}>{entry.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={!autoScroll()}>
|
<Show when={!autoScroll()}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -152,26 +152,42 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const sessionSections = createMemo(() => {
|
const sessionSections = createMemo(() => {
|
||||||
const parentIds: string[] = []
|
const parentItems: SessionListItem[] = []
|
||||||
const childIds: string[] = []
|
const childItems: SessionListItem[] = []
|
||||||
|
|
||||||
for (const [id, session] of props.sessions.entries()) {
|
for (const [id, session] of props.sessions.entries()) {
|
||||||
|
const item: SessionListItem = {
|
||||||
|
id,
|
||||||
|
title: session.title || "Untitled",
|
||||||
|
isActive: id === props.activeSessionId,
|
||||||
|
isParent: session.parentId === null,
|
||||||
|
onSelect: () => props.onSelect(id),
|
||||||
|
onClose: session.parentId === null ? () => props.onClose(id) : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
if (session.parentId === null) {
|
if (session.parentId === null) {
|
||||||
parentIds.push(id)
|
parentItems.push(item)
|
||||||
} else {
|
} else {
|
||||||
childIds.push(id)
|
childItems.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
childIds.sort((a, b) => {
|
childItems.sort((a, b) => {
|
||||||
const sessionA = props.sessions.get(a)
|
const sessionA = props.sessions.get(a.id)
|
||||||
const sessionB = props.sessions.get(b)
|
const sessionB = props.sessions.get(b.id)
|
||||||
if (!sessionA || !sessionB) return 0
|
if (!sessionA || !sessionB) return 0
|
||||||
return sessionB.time.updated - sessionA.time.updated
|
return sessionB.time.updated - sessionA.time.updated
|
||||||
})
|
})
|
||||||
|
|
||||||
parentIds.push("info")
|
parentItems.push({
|
||||||
return { parentIds, childIds }
|
id: "info",
|
||||||
|
title: "Info",
|
||||||
|
isSpecial: true,
|
||||||
|
isActive: props.activeSessionId === "info",
|
||||||
|
onSelect: () => props.onSelect("info"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return { parentItems, childItems }
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -205,48 +221,30 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||||
User Session & Info
|
User Session & Info
|
||||||
</div>
|
</div>
|
||||||
<For each={sessionSections().parentIds}>
|
<For each={sessionSections().parentItems}>
|
||||||
{(id) => {
|
{(item) => (
|
||||||
if (id === "info") {
|
<div class="session-list-item group">
|
||||||
return (
|
<button
|
||||||
<div class="session-list-item group">
|
class={`session-item-base ${
|
||||||
<button
|
item.isActive ? "session-item-active" : "session-item-inactive"
|
||||||
class={`session-item-base ${
|
} ${item.isSpecial ? "session-item-special" : ""}`}
|
||||||
props.activeSessionId === "info" ? "session-item-active" : "session-item-inactive"
|
onClick={item.onSelect}
|
||||||
} session-item-special`}
|
title={item.title}
|
||||||
onClick={() => props.onSelect("info")}
|
role="button"
|
||||||
title="Info"
|
aria-selected={item.isActive}
|
||||||
role="button"
|
>
|
||||||
aria-selected={props.activeSessionId === "info"}
|
<Show when={item.isSpecial} fallback={<MessageSquare class="w-4 h-4 flex-shrink-0" />}>
|
||||||
>
|
<Info class="w-4 h-4 flex-shrink-0" />
|
||||||
<Info class="w-4 h-4 flex-shrink-0" />
|
</Show>
|
||||||
<span class="session-item-title truncate">Info</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = props.sessions.get(id)
|
<span class="session-item-title truncate">{item.title}</span>
|
||||||
if (!session) return null
|
|
||||||
|
|
||||||
return (
|
<Show when={!item.isSpecial && item.onClose}>
|
||||||
<div class="session-list-item group">
|
|
||||||
<button
|
|
||||||
class={`session-item-base ${
|
|
||||||
id === props.activeSessionId ? "session-item-active" : "session-item-inactive"
|
|
||||||
}`}
|
|
||||||
onClick={() => props.onSelect(id)}
|
|
||||||
title={session.title || "Untitled"}
|
|
||||||
role="button"
|
|
||||||
aria-selected={id === props.activeSessionId}
|
|
||||||
>
|
|
||||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
|
||||||
<span class="session-item-title truncate">{session.title || "Untitled"}</span>
|
|
||||||
<span
|
<span
|
||||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
props.onClose(id)
|
item.onClose?.()
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -254,40 +252,51 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<X class="w-3 h-3" />
|
<X class="w-3 h-3" />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Show>
|
||||||
</div>
|
</button>
|
||||||
)
|
</div>
|
||||||
}}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={sessionSections().childIds.length > 0}>
|
<Show when={sessionSections().childItems.length > 0}>
|
||||||
<div class="session-section">
|
<div class="session-section">
|
||||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||||
Agent Sessions
|
Agent Sessions
|
||||||
</div>
|
</div>
|
||||||
<For each={sessionSections().childIds}>
|
<For each={sessionSections().childItems}>
|
||||||
{(id) => {
|
{(item) => (
|
||||||
const session = props.sessions.get(id)
|
<div class="session-list-item group">
|
||||||
if (!session) return null
|
<button
|
||||||
|
class={`session-item-base ${
|
||||||
|
item.isActive ? "session-item-active" : "session-item-inactive"
|
||||||
|
} ${item.isSpecial ? "session-item-special" : ""}`}
|
||||||
|
onClick={item.onSelect}
|
||||||
|
title={item.title}
|
||||||
|
role="button"
|
||||||
|
aria-selected={item.isActive}
|
||||||
|
>
|
||||||
|
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||||
|
|
||||||
return (
|
<span class="session-item-title truncate">{item.title}</span>
|
||||||
<div class="session-list-item group">
|
|
||||||
<button
|
<Show when={!item.isSpecial && item.onClose}>
|
||||||
class={`session-item-base ${
|
<span
|
||||||
id === props.activeSessionId ? "session-item-active" : "session-item-inactive"
|
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||||
}`}
|
onClick={(event) => {
|
||||||
onClick={() => props.onSelect(id)}
|
event.stopPropagation()
|
||||||
title={session.title || "Untitled"}
|
item.onClose?.()
|
||||||
role="button"
|
}}
|
||||||
aria-selected={id === props.activeSessionId}
|
role="button"
|
||||||
>
|
tabIndex={0}
|
||||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
aria-label="Close session"
|
||||||
<span class="session-item-title truncate">{session.title || "Untitled"}</span>
|
>
|
||||||
</button>
|
<X class="w-3 h-3" />
|
||||||
</div>
|
</span>
|
||||||
)
|
</Show>
|
||||||
}}
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
Reference in New Issue
Block a user