Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
04fe786
feat(chat): implement message regeneration and enhance datasource nav…
medazizktata25 Mar 3, 2026
b1b6e42
refactor: clean up code formatting and improve readability across mul…
medazizktata25 Mar 3, 2026
c00a83e
feat: enhance ToolErrorVisualizer with error details formatting and c…
medazizktata25 Mar 3, 2026
9112924
feat: enhance suggestion detection and rendering in Streamdown component
medazizktata25 Mar 3, 2026
84cb935
refactor(conversation): re-add load more buttonfunctionality in conve…
medazizktata25 Mar 3, 2026
8918e41
feat(code-block): enhance CodeBlock component with additional props f…
medazizktata25 Mar 4, 2026
c3529aa
feat(datasource): enhance DatasourceBadge and DatasourceSelector comp…
medazizktata25 Mar 4, 2026
65f3af0
feat: introduce ToolWithTaskDelimiter component and enhance message r…
medazizktata25 Mar 4, 2026
d75c0ea
feat(schema): enhance GetSchemaTool with improved error handling and …
medazizktata25 Mar 4, 2026
2cf2004
fix: simplify condition for checking todos in output object
medazizktata25 Mar 4, 2026
bc86652
feat: Add text highlighting and truncation utilities, implement messa…
medazizktata25 Mar 10, 2026
0a18f14
feat: Resolve merge conflicts, refine conversation list layout, and e…
medazizktata25 Mar 10, 2026
77c788e
feat: enhance tool titles to support ReactNode and display chart type…
medazizktata25 Mar 10, 2026
b017af4
refactor: Improve tool name and chart type extraction to support `Too…
medazizktata25 Mar 10, 2026
877df44
feat: Implement model selection and management UI
medazizktata25 Mar 11, 2026
6e34c5f
feat: Implement web search functionality with preferred search engine…
medazizktata25 Mar 11, 2026
6a41708
refactor: checks fixes
medazizktata25 Mar 11, 2026
940957a
refactor: simplify badge visibility conditions and reformat code in `…
medazizktata25 Mar 11, 2026
1cffb51
chore: agent-ui code file lint fix
medazizktata25 Mar 11, 2026
4b5a757
chore: merge origin/main into feat/tools-handling-and-ui
medazizktata25 Mar 13, 2026
86b285b
fix(ui): resolve searchEngine type mismatch in PromptInputInner
medazizktata25 Mar 13, 2026
2a1432c
feat: Enhance UI for message previews, schema visualization, and load…
medazizktata25 Mar 13, 2026
74a2d8c
fix(ui): adjust button class order in UserMessageBubble
medazizktata25 Mar 13, 2026
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
12 changes: 12 additions & 0 deletions apps/server/src/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type UIMessage,
} from '@qwery/agent-factory-sdk';
import { normalizeUIRole } from '@qwery/shared/message-role-utils';
import { MessageRole } from '@qwery/domain/entities';
import type { Repositories } from '@qwery/domain/repositories';
import { createRepositories } from '../lib/repositories';
import { getTelemetry } from '../lib/telemetry';
Expand Down Expand Up @@ -52,6 +53,17 @@ export function createChatRoutes() {
const model = body.model ?? getDefaultModel();

const repositories = await getRepositories();
if (body.trigger === 'regenerate-message') {
const conversation = await repositories.conversation.findBySlug(slug);
if (conversation) {
const convMessages =
await repositories.message.findByConversationId(conversation.id);
const lastMessage = convMessages.at(-1);
if (lastMessage && lastMessage.role === MessageRole.ASSISTANT) {
await repositories.message.delete(lastMessage.id);
}
}
}
const datasources = await resolveChatDatasources({
bodyDatasources: body.datasources,
messages,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const projectLayout = layout('routes/project/layout.tsx', [
const datasourceLayout = layout('routes/datasource/layout.tsx', [
route('ds/:slug', 'routes/datasource/index.tsx'),
route('ds/:slug/tables', 'routes/datasource/tables.tsx'),
route('ds/:slug/tables/:id', 'routes/datasource/table.tsx'),
route('ds/:slug/tables/:schema/:tableName', 'routes/datasource/table.tsx'),
route('ds/:slug/schema', 'routes/datasource/schema.tsx'),
route('ds/:slug/settings', 'routes/datasource/settings.tsx'),
]);
Expand Down
25 changes: 17 additions & 8 deletions apps/web/app/routes/datasource/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export async function loader(args: Route.LoaderArgs) {
export default function TablePage(props: Route.ComponentProps) {
const params = useParams();
const slug = params.slug as string;
const tableId = params.id as string;
const schemaParam = params.schema as string;
const tableNameParam = params.tableName as string;
const schema = schemaParam ? decodeURIComponent(schemaParam) : '';
const tableName = tableNameParam ? decodeURIComponent(tableNameParam) : '';
const { t } = useTranslation();
const { datasource } = props.loaderData;

Expand All @@ -43,18 +46,25 @@ export default function TablePage(props: Route.ComponentProps) {
});

const table = useMemo(() => {
if (!metadata?.tables) return null;
if (!metadata?.tables || !schema || !tableName) return null;
const tables = metadata.tables as Table[];
return tables.find((t) => t.id.toString() === tableId) || null;
}, [metadata, tableId]);
return (
tables.find(
(t) => (t.schema ?? 'main') === schema && t.name === tableName,
) ?? null
);
}, [metadata, schema, tableName]);

const columns = useMemo(() => {
if (!metadata?.columns || !table) return [];
const allColumns = metadata.columns as Column[];
return allColumns.filter(
(col) => col.table_id.toString() === tableId && col.table === table.name,
(col) =>
col.table_id === table.id &&
col.table === table.name &&
(col.schema ?? 'main') === (table.schema ?? 'main'),
);
}, [metadata, table, tableId]);
}, [metadata, table]);

const columnListItems: ColumnListItem[] = useMemo(() => {
return columns.map((col) => ({
Expand Down Expand Up @@ -89,7 +99,6 @@ export default function TablePage(props: Route.ComponentProps) {
);
}

const tableName = table.name;
const tablesPath = `/ds/${slug}/tables`;

return (
Expand All @@ -100,7 +109,7 @@ export default function TablePage(props: Route.ComponentProps) {
defaultValue: 'Tables',
})}
</Link>{' '}
<span className="text-muted-foreground">&gt;</span> {tableName}
<span className="text-muted-foreground">&gt;</span> {table.name}
</h1>
<Columns columns={columnListItems} />
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/routes/datasource/tables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export default function TablesPage(props: Route.ComponentProps) {
const handleTableClick = (table: TableListItem) => {
const tableData = filteredTables.find((t) => t.name === table.tableName);
if (!tableData) return;

const tablePath = `/ds/${slug}/tables/${tableData.id}`;
navigate(tablePath);
const schema = encodeURIComponent(tableData.schema ?? 'main');
const tableName = encodeURIComponent(tableData.name);
navigate(`/ds/${slug}/tables/${schema}/${tableName}`);
};

if (!datasource) {
Expand Down
46 changes: 37 additions & 9 deletions apps/web/app/routes/project/_components/agent-ui-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ import { useAgentStatus, formatRelativeTime } from '@qwery/ui/ai';
import type { FeedbackPayload } from '@qwery/ui/ai';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { createDatasourceViewPath } from '~/config/project.navigation.config';
import {
createDatasourceViewPath,
createDatasourceTableViewPath,
} from '~/config/project.navigation.config';
import {
openDatasourceInNewTab,
openTableInNewTab,
} from '~/lib/utils/datasource-navigation';

type SendMessageFn = (
message: { text: string },
Expand Down Expand Up @@ -693,14 +700,32 @@ export const AgentUIWrapper = forwardRef<
);

const _handleDatasourceNameClick = useCallback(
(idOrSlug: string, _name: string) => {
const ds =
datasourceItems.find((d) => d.id === idOrSlug) ??
datasourceItems.find((d) => d.slug === idOrSlug);
if (ds?.slug) {
const path = createDatasourceViewPath(ds.slug);
window.open(path, '_blank', 'noopener,noreferrer');
}
(idOrSlug: string, name: string) => {
openDatasourceInNewTab(
datasourceItems,
idOrSlug,
name,
createDatasourceViewPath,
);
},
[datasourceItems],
);

const _handleTableNameClick = useCallback(
(
datasourceIdOrSlug: string,
datasourceName: string,
schema: string,
tableName: string,
) => {
openTableInNewTab(
datasourceItems,
datasourceIdOrSlug,
datasourceName,
schema,
tableName,
createDatasourceTableViewPath,
);
},
[datasourceItems],
);
Expand Down Expand Up @@ -733,6 +758,9 @@ export const AgentUIWrapper = forwardRef<
notebookContext={notebookContext}
isLoading={isLoading}
conversationSlug={conversationSlug}
onDatasourceNameClick={_handleDatasourceNameClick}
onTableNameClick={_handleTableNameClick}
getDatasourceTooltip={_getDatasourceTooltip}
/>
<AlertDialog
open={noDatasourceDialogOpen}
Expand Down
27 changes: 24 additions & 3 deletions apps/web/app/routes/project/conversation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,15 @@ export default function ConversationIndexPage() {
const isLoading = isLoadingConversations || isProjectLoading;
const [searchQuery, setSearchQuery] = useState('');
const [isEditMode, setIsEditMode] = useState(false);
const [loadMoreState, setLoadMoreState] = useState<{
hasMore: boolean;
onLoadMore: () => void;
isLoading: boolean;
} | null>(null);

return (
<div className="bg-background flex h-screen w-full flex-col overflow-hidden">
<div className="flex h-full flex-col">
<div className="bg-background flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 flex-col">
<section className="flex shrink-0 flex-col gap-6 px-8 py-6 lg:px-16 lg:py-10">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">{t('chat:title')}</h1>
Expand Down Expand Up @@ -234,7 +239,7 @@ export default function ConversationIndexPage() {
</div>
</section>

<div className="min-h-0 flex-1 overflow-hidden px-8 lg:px-16">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-8 lg:px-16">
{isLoading ? (
<div className="bg-muted/10 h-full w-full animate-pulse rounded-2xl" />
) : (
Expand All @@ -257,10 +262,26 @@ export default function ConversationIndexPage() {
onSearchQueryChange={setSearchQuery}
isEditMode={isEditMode}
onEditModeChange={setIsEditMode}
onLoadMoreStateChange={setLoadMoreState}
className="h-full"
/>
)}
</div>

{loadMoreState?.hasMore && (
<div className="bg-background flex shrink-0 items-center px-8 py-4 lg:px-16 lg:py-6">
<Button
variant="outline"
size="sm"
onClick={loadMoreState.onLoadMore}
disabled={loadMoreState.isLoading}
className="text-muted-foreground hover:text-foreground h-10 w-full border-2"
data-test="conversation-load-more"
>
{loadMoreState.isLoading ? 'Loading...' : 'Load more'}
</Button>
</div>
)}
</div>
</div>
);
Expand Down
12 changes: 12 additions & 0 deletions apps/web/config/project.navigation.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,15 @@ export function createDatasourcePath(slug: string, name: string) {
export function createDatasourceViewPath(slug: string) {
return createPath(pathsConfig.app.projectDatasourceView, slug);
}

/** Build path to a specific table: /ds/{slug}/tables/{schema}/{tableName} */
export function createDatasourceTableViewPath(
slug: string,
schema: string,
tableName: string,
) {
const base = createPath(pathsConfig.app.datasourceTables, slug);
const encodedSchema = encodeURIComponent(schema || 'main');
const encodedTableName = encodeURIComponent(tableName);
return `${base}/${encodedSchema}/${encodedTableName}`;
}
81 changes: 81 additions & 0 deletions apps/web/lib/utils/datasource-navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Resolves a datasource by id, slug, or name and opens its view in a new tab.
* Used by getSchema UI (datasource names, error cards, minimal table names) and any other link that should open the datasource page.
*/

export interface DatasourceItemLike {
id: string;
name?: string;
slug?: string;
}

function slugify(s: string): string {
return (
String(s)
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9_-]/g, '') || s
);
}

export function resolveDatasource(
items: DatasourceItemLike[],
idOrSlug: string,
name: string,
): DatasourceItemLike | undefined {
return (
items.find((d) => d.id === idOrSlug) ??
items.find((d) => d.slug === idOrSlug) ??
items.find(
(d) =>
slugify(d.name ?? '') === idOrSlug ||
slugify(d.slug ?? '') === idOrSlug,
) ??
items.find((d) => d.name === name)
);
}

/**
* Resolves the datasource and opens its view URL in a new tab.
* @param getPath - e.g. createDatasourceViewPath from project.navigation.config
*/
export function openDatasourceInNewTab(
items: DatasourceItemLike[],
idOrSlug: string,
name: string,
getPath: (slug: string) => string,
): void {
const ds = resolveDatasource(items, idOrSlug, name);
if (ds?.slug) {
const path = getPath(ds.slug);
openUrlInNewTab(path);
}
}

/**
* Opens a relative or absolute URL in a new tab.
*/
export function openUrlInNewTab(pathOrUrl: string): void {
const url = pathOrUrl.startsWith('http')
? pathOrUrl
: `${typeof window !== 'undefined' ? window.location.origin : ''}${pathOrUrl}`;
window.open(url, '_blank', 'noopener,noreferrer');
}

/**
* Resolves the datasource and opens the table view URL in a new tab.
* @param getTablePath - e.g. createDatasourceTableViewPath(slug, schema, tableName)
*/
export function openTableInNewTab(
items: DatasourceItemLike[],
datasourceIdOrSlug: string,
datasourceName: string,
schema: string,
tableName: string,
getTablePath: (slug: string, schema: string, tableName: string) => string,
): void {
const ds = resolveDatasource(items, datasourceIdOrSlug, datasourceName);
if (ds?.slug) {
const path = getTablePath(ds.slug, schema || 'main', tableName);
openUrlInNewTab(path);
}
}
33 changes: 33 additions & 0 deletions apps/web/styles/qwery.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
/* Scrollbar visible on hover - overrides base scrollbar styles */
.scrollbar-hover-visible {
scrollbar-color: transparent transparent;
}
.scrollbar-hover-visible:hover,
.group:hover .scrollbar-hover-visible {
scrollbar-color: rgba(155, 155, 155, 0.3) transparent;
}
.dark .scrollbar-hover-visible:hover,
.dark .group:hover .scrollbar-hover-visible {
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
}
.scrollbar-hover-visible::-webkit-scrollbar {
height: 5px !important;
width: 5px !important;
background: transparent !important;
}
.scrollbar-hover-visible::-webkit-scrollbar-track {
background: transparent !important;
}
.scrollbar-hover-visible::-webkit-scrollbar-thumb {
background: transparent !important;
border-radius: 10px;
}
.scrollbar-hover-visible:hover::-webkit-scrollbar-thumb,
.group:hover .scrollbar-hover-visible::-webkit-scrollbar-thumb {
background: rgba(155, 155, 155, 0.3) !important;
}
.dark .scrollbar-hover-visible:hover::-webkit-scrollbar-thumb,
.dark .group:hover .scrollbar-hover-visible::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15) !important;
}

[data-radix-popper-content-wrapper] {
@apply w-full md:w-auto;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/agent-factory-sdk/src/services/default-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const defaultTransport = (api: string) =>
new DefaultChatTransport({
api,
prepareSendMessagesRequest: (request) => {
const { messages, body = {} } = request;
const { messages, body = {}, trigger } = request;
const lastUserMessageIndex = messages.findLastIndex(
(m) => normalizeUIRole(m.role) === 'user',
);
Expand All @@ -15,6 +15,7 @@ export const defaultTransport = (api: string) =>
body: {
...body,
messages: lastUserMessage ? [lastUserMessage] : [],
...(trigger ? { trigger } : {}),
},
};
},
Expand Down
Loading
Loading