Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions apps/codex-claw/src/routes/api/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,19 @@ import {
export const Route = createFileRoute('/api/sessions')({
server: {
handlers: {
GET: async () => {
GET: async ({ request }) => {
try {
return json(await listCodexSessions())
const url = new URL(request.url)
return json(
await listCodexSessions({
query: url.searchParams.get('q') ?? undefined,
filter: url.searchParams.get('filter') ?? undefined,
tag: url.searchParams.get('tag') ?? undefined,
includeArchived:
url.searchParams.get('includeArchived') === '1' ||
url.searchParams.get('includeArchived') === 'true',
}),
)
} catch (err) {
return json(
{
Expand Down Expand Up @@ -62,6 +72,11 @@ export const Route = createFileRoute('/api/sessions')({
typeof body.friendlyId === 'string' ? body.friendlyId.trim() : ''
const label =
typeof body.label === 'string' ? body.label.trim() : undefined
const tags = Array.isArray(body.tags)
? body.tags.filter((tag): tag is string => typeof tag === 'string')
: undefined
const archived =
typeof body.archived === 'boolean' ? body.archived : undefined
let sessionKey = rawSessionKey

if (!sessionKey && rawFriendlyId) {
Expand All @@ -76,7 +91,12 @@ export const Route = createFileRoute('/api/sessions')({
)
}

const payload = await patchCodexSession({ key: sessionKey, label })
const payload = await patchCodexSession({
key: sessionKey,
label,
tags,
archived,
})
return json({
ok: true,
sessionKey: payload.key,
Expand Down
65 changes: 64 additions & 1 deletion apps/codex-claw/src/screens/chat/chat-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
RepoContextSelection,
SessionListResponse,
SessionMeta,
SessionSummary,
TaskListResponse,
WorkspaceListResponse,
WorkspaceSummary,
Expand All @@ -21,20 +22,82 @@ type GatewayStatusResponse = {

export const chatQueryKeys = {
sessions: ['chat', 'sessions'] as const,
sessionSearch: function sessionSearch(params: SessionSearchParams) {
return [
'chat',
'session-search',
params.query ?? '',
params.filter ?? 'workspace',
params.tag ?? '',
] as const
},
workspaces: ['chat', 'workspaces'] as const,
tasks: ['chat', 'tasks'] as const,
history: function history(friendlyId: string, sessionKey: string) {
return ['chat', 'history', friendlyId, sessionKey] as const
},
} as const

export type SessionSearchFilter =
| 'workspace'
| 'pinned'
| 'recent'
| 'failed'
| 'tagged'
| 'archived'

export type SessionSearchParams = {
query?: string
filter?: SessionSearchFilter
tag?: string
signal?: AbortSignal
}

export async function fetchSessions(): Promise<Array<SessionMeta>> {
const res = await fetch('/api/sessions')
const res = await fetch('/api/sessions?includeArchived=1')
if (!res.ok) throw new Error(await readError(res))
const data = (await res.json()) as SessionListResponse
return normalizeSessions(data.sessions)
}

export async function fetchSessionSearch(
params: SessionSearchParams,
): Promise<Array<SessionMeta>> {
const query = new URLSearchParams({ includeArchived: '1' })
const rawQuery = params.query?.trim() ?? ''
const filter = params.filter ?? 'workspace'
if (rawQuery) query.set('q', rawQuery)
if (filter !== 'workspace' && filter !== 'pinned') {
query.set('filter', filter)
}
if (params.tag?.trim()) query.set('tag', params.tag.trim())
const res = await fetch('/api/sessions?' + query.toString(), {
signal: params.signal,
})
if (!res.ok) throw new Error(await readError(res))
const data = (await res.json()) as SessionListResponse
return normalizeSessions(data.sessions)
}

export async function updateSessionMetadata(payload: {
sessionKey: string
tags?: Array<string>
archived?: boolean
}): Promise<SessionMeta> {
const res = await fetch('/api/sessions', {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) throw new Error(await readError(res))
const data = (await res.json()) as { entry?: SessionSummary }
if (!data.entry) {
throw new Error('Session metadata update returned no session.')
}
const [session] = normalizeSessions([data.entry])
return session
}

export async function fetchHistory(payload: {
sessionKey: string
friendlyId: string
Expand Down
49 changes: 49 additions & 0 deletions apps/codex-claw/src/screens/chat/components/chat-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { Link, useNavigate } from '@tanstack/react-router'
import { useChatSettings } from '../hooks/use-chat-settings'
import { useDeleteSession } from '../hooks/use-delete-session'
import { useRenameSession } from '../hooks/use-rename-session'
import { useSessionMetadata } from '../hooks/use-session-metadata'
import { useSessionShortcuts } from '../hooks/use-session-shortcuts'
import { SettingsDialog } from './settings-dialog'
import { SessionRenameDialog } from './sidebar/session-rename-dialog'
import { SessionDeleteDialog } from './sidebar/session-delete-dialog'
import { SessionTagsDialog } from './sidebar/session-tags-dialog'
import { SidebarSessions } from './sidebar/sidebar-sessions'
import { CommandSessionDialog } from './command-session'
import type { SessionMeta } from '../types'
Expand Down Expand Up @@ -72,6 +74,7 @@ function ChatSidebarComponent({
} = useChatSettings()
const { deleteSession } = useDeleteSession()
const { renameSession } = useRenameSession()
const { updateMetadata } = useSessionMetadata()
const transition = {
duration: 0.15,
ease: isCollapsed ? 'easeIn' : 'easeOut',
Expand All @@ -85,6 +88,10 @@ function ChatSidebarComponent({
const [deleteSessionKey, setDeleteSessionKey] = useState<string | null>(null)
const [deleteFriendlyId, setDeleteFriendlyId] = useState<string | null>(null)
const [deleteSessionTitle, setDeleteSessionTitle] = useState('')
const [tagsDialogOpen, setTagsDialogOpen] = useState(false)
const [tagsSessionKey, setTagsSessionKey] = useState<string | null>(null)
const [tagsSessionTitle, setTagsSessionTitle] = useState('')
const [tagsSessionTags, setTagsSessionTags] = useState<Array<string>>([])
const [searchDialogOpen, setSearchDialogOpen] = useState(false)
const navigate = useNavigate()

Expand Down Expand Up @@ -122,6 +129,34 @@ function ChatSidebarComponent({
setRenameSessionKey(null)
}

function handleOpenTags(session: SessionMeta) {
setTagsSessionKey(session.key)
setTagsSessionTitle(
session.label ||
session.title ||
session.derivedTitle ||
session.friendlyId,
)
setTagsSessionTags(session.tags)
setTagsDialogOpen(true)
}

function handleSaveTags(tags: Array<string>) {
if (tagsSessionKey) {
void updateMetadata({ sessionKey: tagsSessionKey, tags })
}
setTagsDialogOpen(false)
setTagsSessionKey(null)
setTagsSessionTags([])
}

function handleToggleArchive(session: SessionMeta) {
void updateMetadata({
sessionKey: session.key,
archived: !session.archived,
})
}

function handleOpenDelete(session: SessionMeta) {
setDeleteSessionKey(session.key)
setDeleteFriendlyId(session.friendlyId)
Expand Down Expand Up @@ -312,6 +347,8 @@ function ChatSidebarComponent({
activeFriendlyId={activeFriendlyId}
onSelect={onSelectSession}
onRename={handleOpenRename}
onEditTags={handleOpenTags}
onToggleArchive={handleToggleArchive}
onDelete={handleOpenDelete}
/>
</div>
Expand Down Expand Up @@ -385,6 +422,15 @@ function ChatSidebarComponent({
onCancel={() => setRenameDialogOpen(false)}
/>

<SessionTagsDialog
open={tagsDialogOpen}
onOpenChange={setTagsDialogOpen}
sessionTitle={tagsSessionTitle}
tags={tagsSessionTags}
onSave={handleSaveTags}
onCancel={() => setTagsDialogOpen(false)}
/>

<SessionDeleteDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
Expand All @@ -410,6 +456,9 @@ function areSessionsEqual(
if (prev.label !== next.label) return false
if (prev.title !== next.title) return false
if (prev.derivedTitle !== next.derivedTitle) return false
if (prev.archived !== next.archived) return false
if (prev.hasFailedRun !== next.hasFailedRun) return false
if (prev.tags.join(',') !== next.tags.join(',')) return false
if (prev.updatedAt !== next.updatedAt) return false
}
return true
Expand Down
Loading
Loading