From f357ae298d6d0b29ce5727afd215417a074fc484 Mon Sep 17 00:00:00 2001 From: alashchev17 Date: Wed, 11 Dec 2024 15:17:28 +0100 Subject: [PATCH 1/5] fix: recommended integrations chat links open an intermediate page in integrations UI --- .../IntegrationsView/IntegrationsView.tsx | 77 ++++++++++++++----- src/components/SmartLink/SmartLink.tsx | 2 +- src/features/Chat/Thread/types.ts | 1 + src/features/Pages/pagesSlice.ts | 1 + src/hooks/useGoToLink.ts | 24 ++++-- src/hooks/useLinksFromLsp.ts | 37 ++++++++- src/services/refact/integrations.ts | 4 + 7 files changed, 114 insertions(+), 32 deletions(-) diff --git a/src/components/IntegrationsView/IntegrationsView.tsx b/src/components/IntegrationsView/IntegrationsView.tsx index baed17be..af6f52d2 100644 --- a/src/components/IntegrationsView/IntegrationsView.tsx +++ b/src/components/IntegrationsView/IntegrationsView.tsx @@ -28,6 +28,7 @@ import { Integration, integrationsApi, IntegrationWithIconRecord, + IntegrationWithIconRecordAndAddress, IntegrationWithIconResponse, isDetailMessage, isNotConfiguredIntegrationWithIconRecord, @@ -80,42 +81,78 @@ export const IntegrationsView: FC = ({ const maybeIntegration = useMemo(() => { if (!currentThreadIntegration) return null; if (!integrationsMap) return null; + debugIntegrations( + `[DEBUG]: currentThreadIntegration: `, + currentThreadIntegration, + ); // TODO: check for extra flag in currentThreadIntegration to return different find() call from notConfiguredGrouped integrations if it's set to true - return ( + const integration = integrationsMap.integrations.find( (integration) => - integration.integr_config_path === - currentThreadIntegration.integrationPath, - ) ?? null - ); + currentThreadIntegration.integrationName?.startsWith("cmdline") + ? integration.integr_name === "cmdline_TEMPLATE" + : integration.integr_name === + currentThreadIntegration.integrationName, + // integration.integr_name === currentThreadIntegration.integrationName, + ) ?? null; + if (!integration) return null; + + const integrationWithFlag = { + ...integration, + shouldIntermediatePageShowUp: + currentThreadIntegration.shouldIntermediatePageShowUp ?? false, + } as IntegrationWithIconRecordAndAddress; + return integrationWithFlag; }, [currentThreadIntegration, integrationsMap]); // TBD: what if they went home then came back to integrations? const [currentIntegration, setCurrentIntegration] = - useState(maybeIntegration); + useState( + maybeIntegration?.shouldIntermediatePageShowUp ? null : maybeIntegration, + ); const [currentNotConfiguredIntegration, setCurrentNotConfiguredIntegration] = useState(null); // TODO: uncomment when ready - // useEffect(() => { - // if (maybeIntegration) { - // if (maybeIntegration.shouldBeOpenedOnIntermediatePage) { - // setNotConfiguredIntegration(maybeIntegration); - // setCurrentIntegration(null); - // } else { - // setCurrentIntegration(maybeIntegration); - // setNotConfiguredIntegration(null); - // } - // } - // }, [maybeIntegration]); - useEffect(() => { if (maybeIntegration) { - setCurrentIntegration(maybeIntegration); + if (maybeIntegration.shouldIntermediatePageShowUp) { + setCurrentNotConfiguredIntegration(() => { + const similarIntegrations = integrationsMap?.integrations.filter( + (integr) => integr.integr_name === maybeIntegration.integr_name, + ); + if (!similarIntegrations) return null; + const uniqueConfigPaths = Array.from( + new Set( + similarIntegrations.map((integr) => integr.integr_config_path), + ), + ); + const uniqueProjectPaths = Array.from( + new Set(similarIntegrations.map((integr) => integr.project_path)), + ); + + uniqueProjectPaths.sort((a, _b) => (a === "" ? -1 : 1)); + uniqueConfigPaths.sort((a, _b) => (a.includes(".config") ? -1 : 1)); + + const integrationToConfigure: NotConfiguredIntegrationWithIconRecord = + { + ...maybeIntegration, + integr_config_path: uniqueConfigPaths, + project_path: uniqueProjectPaths, + integr_config_exists: false, + }; + + return integrationToConfigure; + }); + setCurrentIntegration(null); + } else { + setCurrentIntegration(maybeIntegration); + setCurrentNotConfiguredIntegration(null); + } } - }, [maybeIntegration]); + }, [maybeIntegration, integrationsMap?.integrations]); const [currentIntegrationSchema, setCurrentIntegrationSchema] = useState< Integration["integr_schema"] | null diff --git a/src/components/SmartLink/SmartLink.tsx b/src/components/SmartLink/SmartLink.tsx index a91a32fe..1e8d5a32 100644 --- a/src/components/SmartLink/SmartLink.tsx +++ b/src/components/SmartLink/SmartLink.tsx @@ -26,7 +26,7 @@ export const SmartLink: FC<{ const handleClick = useCallback(() => { if (sl_goto) { - handleGoTo(sl_goto); + handleGoTo({ goto: sl_goto }); return; } if (sl_chat) { diff --git a/src/features/Chat/Thread/types.ts b/src/features/Chat/Thread/types.ts index 24ebdbb2..ff713e0d 100644 --- a/src/features/Chat/Thread/types.ts +++ b/src/features/Chat/Thread/types.ts @@ -6,6 +6,7 @@ export type IntegrationMeta = { name?: string; path?: string; project?: string; + shouldIntermediatePageShowUp?: boolean; }; export type ChatThread = { id: string; diff --git a/src/features/Pages/pagesSlice.ts b/src/features/Pages/pagesSlice.ts index d4a19e65..c630a12c 100644 --- a/src/features/Pages/pagesSlice.ts +++ b/src/features/Pages/pagesSlice.ts @@ -59,6 +59,7 @@ export interface IntegrationsSetupPage { projectPath?: string; integrationName?: string; integrationPath?: string; + shouldIntermediatePageShowUp?: boolean; } export type Page = diff --git a/src/hooks/useGoToLink.ts b/src/hooks/useGoToLink.ts index e7051c2e..372df2cc 100644 --- a/src/hooks/useGoToLink.ts +++ b/src/hooks/useGoToLink.ts @@ -5,6 +5,7 @@ import { useAppDispatch } from "./useAppDispatch"; import { popBackTo } from "../features/Pages/pagesSlice"; import { useAppSelector } from "./useAppSelector"; import { selectIntegration } from "../features/Chat/Thread/selectors"; +import { debugIntegrations } from "../debugConfig"; export function useGoToLink() { const dispatch = useAppDispatch(); @@ -12,7 +13,7 @@ export function useGoToLink() { const maybeIntegration = useAppSelector(selectIntegration); const handleGoTo = useCallback( - (goto?: string) => { + ({ goto }: { goto?: string }) => { if (!goto) return; // TODO: duplicated in smart links. const [action, payload] = goto.split(":"); @@ -24,6 +25,11 @@ export function useGoToLink() { } case "settings": { const isFile = isAbsolutePath(payload); + debugIntegrations(`[DEBUG]: maybeIntegration: `, maybeIntegration); + if (!maybeIntegration) { + debugIntegrations(`[DEBUG]: integration data is not available.`); + return; + } dispatch( popBackTo({ name: "integrations page", @@ -31,9 +37,11 @@ export function useGoToLink() { integrationName: !isFile && payload !== "DEFAULT" ? payload - : maybeIntegration?.name, - integrationPath: isFile ? payload : maybeIntegration?.path, - projectPath: maybeIntegration?.project, + : maybeIntegration.name, + integrationPath: isFile ? payload : maybeIntegration.path, + projectPath: maybeIntegration.project, + shouldIntermediatePageShowUp: + maybeIntegration.shouldIntermediatePageShowUp, }), ); // TODO: open in the integrations @@ -48,9 +56,11 @@ export function useGoToLink() { }, [ dispatch, - maybeIntegration?.name, - maybeIntegration?.path, - maybeIntegration?.project, + // maybeIntegration?.name, + // maybeIntegration?.path, + // maybeIntegration?.project, + // maybeIntegration?.shouldIntermediatePageShowUp, + maybeIntegration, queryPathThenOpenFile, ], ); diff --git a/src/hooks/useLinksFromLsp.ts b/src/hooks/useLinksFromLsp.ts index 9cd9b69f..7464812c 100644 --- a/src/hooks/useLinksFromLsp.ts +++ b/src/hooks/useLinksFromLsp.ts @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { diffApi, isUserMessage, @@ -20,6 +20,7 @@ import { setIntegrationData, } from "../features/Chat"; import { useGoToLink } from "./useGoToLink"; +import { debugIntegrations } from "../debugConfig"; export function useLinksFromLsp() { const dispatch = useAppDispatch(); @@ -52,19 +53,47 @@ export function useLinksFromLsp() { return false; }, [messages]); + const [pendingIntegrationGoto, setPendingIntegrationGoto] = useState< + string | null + >(null); + + useEffect(() => { + if ( + typeof maybeIntegration?.shouldIntermediatePageShowUp !== "undefined" && + pendingIntegrationGoto + ) { + handleGoTo({ goto: pendingIntegrationGoto }); + setPendingIntegrationGoto(null); + } + }, [pendingIntegrationGoto, handleGoTo, maybeIntegration]); + const handleLinkAction = useCallback( (link: ChatLink) => { if (!("action" in link)) return; - if (link.action === "goto" && "goto" in link) { - handleGoTo(link.goto); + const [action, payload] = link.goto.split(":"); + if (action.toLowerCase() === "settings") { + debugIntegrations( + `[DEBUG]: this goto is integrations one, dispatching integration data`, + ); + dispatch( + setIntegrationData({ + name: payload, + shouldIntermediatePageShowUp: true, + }), + ); + setPendingIntegrationGoto(link.goto); + } + handleGoTo({ + goto: link.goto, + }); return; } if (link.action === "patch-all") { void applyPatches(messages).then(() => { if ("goto" in link) { - handleGoTo(link.goto); + handleGoTo({ goto: link.goto }); } }); return; diff --git a/src/services/refact/integrations.ts b/src/services/refact/integrations.ts index 1b7e6ff6..f0189eee 100644 --- a/src/services/refact/integrations.ts +++ b/src/services/refact/integrations.ts @@ -499,6 +499,10 @@ export type IntegrationWithIconRecord = { // unparsed: unknown; }; +export type IntegrationWithIconRecordAndAddress = IntegrationWithIconRecord & { + shouldIntermediatePageShowUp?: boolean; +}; + export type NotConfiguredIntegrationWithIconRecord = { project_path: string[]; integr_name: string; From 5d138947dbcc24372cd32cabb7489f95d4773cb4 Mon Sep 17 00:00:00 2001 From: alashchev17 Date: Wed, 11 Dec 2024 15:43:18 +0100 Subject: [PATCH 2/5] fix: ToolConfirmation UI adjustements --- .../ChatForm/ToolConfirmation.module.css | 6 +-- src/components/ChatForm/ToolConfirmation.tsx | 48 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/components/ChatForm/ToolConfirmation.module.css b/src/components/ChatForm/ToolConfirmation.module.css index 1d71faa4..0c01c78f 100644 --- a/src/components/ChatForm/ToolConfirmation.module.css +++ b/src/components/ChatForm/ToolConfirmation.module.css @@ -1,7 +1,7 @@ .ToolConfirmationCard { position: absolute; - width: calc(100% - 80px); - bottom: 40px; + width: calc(100% - 100px); + bottom: 50px; left: 50%; transform: translateX(-50%); @@ -10,7 +10,7 @@ flex-direction: column; gap: 8px; border-radius: 8px; - background-color: var(--color-surface); + background-color: var(--gray-1); min-height: fit-content; & pre { margin: 0; diff --git a/src/components/ChatForm/ToolConfirmation.tsx b/src/components/ChatForm/ToolConfirmation.tsx index 5951c181..f3b93a9e 100644 --- a/src/components/ChatForm/ToolConfirmation.tsx +++ b/src/components/ChatForm/ToolConfirmation.tsx @@ -1,10 +1,14 @@ import React from "react"; import type { PauseReason } from "../../features/ToolConfirmation/confirmationSlice"; -import { useEventsBusForIDE } from "../../hooks"; +import { + useAppDispatch, + // useEventsBusForIDE +} from "../../hooks"; import { Card, Button, Text, Flex } from "@radix-ui/themes"; import { Markdown } from "../Markdown"; import { Link } from "../Link"; import styles from "./ToolConfirmation.module.css"; +import { push } from "../../features/Pages/pagesSlice"; type ToolConfirmationProps = { pauseReasons: PauseReason[]; @@ -18,21 +22,29 @@ const getConfirmationalMessage = ( confirmationalCommands: string[], denialCommands: string[], ) => { - const ruleText = `${rules.join(", ")} ${rules.length > 1 ? "rules" : "rule"}`; + const ruleText = `${rules.join(", ")}`; if (types.every((type) => type === "confirmation")) { - return `Following ${ - commands.length > 1 ? "commands need" : "command needs" - } confirmation due to ${ruleText}.`; + return `${ + commands.length > 1 ? "Commands need" : "Command needs" + } confirmation due to \`\`\`${ruleText}\`\`\` ${ + rules.length > 1 ? "rules" : "rule" + }.`; } else if (types.every((type) => type === "denial")) { - return `Following ${ - commands.length > 1 ? "commands were" : "command was" - } denied due to ${ruleText}.`; + return `${ + commands.length > 1 ? "Commands were" : "Command was" + } denied due to \`\`\`${ruleText}\`\`\` ${ + rules.length > 1 ? "rules" : "rule" + }.`; } else { - return `Following ${ - confirmationalCommands.length > 1 ? "commands need" : "command needs" + return `${ + confirmationalCommands.length > 1 ? "Commands need" : "Command needs" } confirmation: ${confirmationalCommands.join(", ")}.\n\nFollowing ${ denialCommands.length > 1 ? "commands were" : "command was" - } denied: ${denialCommands.join(", ")}.\n\nAll due to ${ruleText}.`; + } denied: ${denialCommands.join( + ", ", + )}.\n\nAll due to \`\`\`${ruleText}\`\`\` ${ + rules.length > 1 ? "rules" : "rule" + }.`; } }; @@ -40,7 +52,9 @@ export const ToolConfirmation: React.FC = ({ pauseReasons, onConfirm, }) => { - const { openIntegrationsFile } = useEventsBusForIDE(); + const dispatch = useAppDispatch(); + + // const { openIntegrationsFile } = useEventsBusForIDE(); const commands = pauseReasons.map((reason) => reason.command); const rules = pauseReasons.map((reason) => reason.rule); @@ -80,15 +94,15 @@ export const ToolConfirmation: React.FC = ({ >{`${"```bash\n"}${command}${"\n```"}`} ))} - {message.concat("\n\n")} - - You can modify the ruleset in{" "} + {message.concat("\n\n")} + + You can modify the ruleset on{" "} { - void openIntegrationsFile(); + dispatch(push({ name: "integrations page" })); }} > - integrations.yaml + Configuration Page From 1fdf7180ac3745939292ca9cc711c4ac95e5efdc Mon Sep 17 00:00:00 2001 From: alashchev17 Date: Wed, 11 Dec 2024 15:48:53 +0100 Subject: [PATCH 3/5] fix: typeguard extracted & docker section renders if docker is present in integr_schema --- .../IntegrationForm/IntegrationForm.tsx | 52 +++++++------------ src/services/refact/docker.ts | 11 ++++ 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx b/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx index a003ccad..e8235899 100644 --- a/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx +++ b/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx @@ -20,18 +20,6 @@ import { toPascalCase } from "../../../utils/toPascalCase"; import { debugIntegrations } from "../../../debugConfig"; import { iconMap } from "../icons/iconMap"; -// TODO: should be extracted in the future -function jsonHasWhenIsolated( - json: unknown, -): json is Record & { when_isolated: boolean } { - return ( - typeof json === "object" && - json !== null && - "when_isolated" in json && - typeof json.when_isolated === "boolean" - ); -} - function areAllFieldsBoolean(json: unknown): json is Record { return ( typeof json === "object" && @@ -257,29 +245,27 @@ export const IntegrationForm: FC = ({ )} - {integration.data.integr_schema.docker && - jsonHasWhenIsolated(integration.data.integr_values.available) && - integration.data.integr_values.available.when_isolated && ( - - - {integration.data.integr_name} - - {toPascalCase(integration.data.integr_name)} Containers - - - + + {integration.data.integr_name} + + {toPascalCase(integration.data.integr_name)} Containers + - )} + + + )} ); }; diff --git a/src/services/refact/docker.ts b/src/services/refact/docker.ts index bd52fc73..6cbd329e 100644 --- a/src/services/refact/docker.ts +++ b/src/services/refact/docker.ts @@ -317,3 +317,14 @@ function isDockerPorts(json: unknown): json is DockerPorts { // Since DockerPorts is defined as NonNullable, we don't have specific structure to validate. Just checking, that it's not null | undefined return json !== null && json !== undefined; } + +export function jsonHasWhenIsolated( + json: unknown, +): json is Record & { when_isolated: boolean } { + return ( + typeof json === "object" && + json !== null && + "when_isolated" in json && + typeof json.when_isolated === "boolean" + ); +} From ca8a79c56dd7a6d2d8a81c5a1d66f0dbe1736613 Mon Sep 17 00:00:00 2001 From: alashchev17 Date: Wed, 11 Dec 2024 16:16:32 +0100 Subject: [PATCH 4/5] fix: handling opening of links from history, not saving integration meta to cache --- src/features/History/historySlice.ts | 1 + src/hooks/useGoToLink.ts | 4 +++- src/hooks/useLinksFromLsp.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/features/History/historySlice.ts b/src/features/History/historySlice.ts index ba57ba4a..864aa698 100644 --- a/src/features/History/historySlice.ts +++ b/src/features/History/historySlice.ts @@ -77,6 +77,7 @@ export const historySlice = createSlice({ : getFirstUserContentFromChat(action.payload.messages), createdAt: action.payload.createdAt ?? now, updatedAt: now, + integration: null, isTitleGenerated: action.payload.isTitleGenerated, }; diff --git a/src/hooks/useGoToLink.ts b/src/hooks/useGoToLink.ts index 372df2cc..d0922072 100644 --- a/src/hooks/useGoToLink.ts +++ b/src/hooks/useGoToLink.ts @@ -41,7 +41,9 @@ export function useGoToLink() { integrationPath: isFile ? payload : maybeIntegration.path, projectPath: maybeIntegration.project, shouldIntermediatePageShowUp: - maybeIntegration.shouldIntermediatePageShowUp, + payload !== "DEFAULT" + ? maybeIntegration.shouldIntermediatePageShowUp + : false, }), ); // TODO: open in the integrations diff --git a/src/hooks/useLinksFromLsp.ts b/src/hooks/useLinksFromLsp.ts index 7464812c..aeebc168 100644 --- a/src/hooks/useLinksFromLsp.ts +++ b/src/hooks/useLinksFromLsp.ts @@ -79,7 +79,7 @@ export function useLinksFromLsp() { dispatch( setIntegrationData({ name: payload, - shouldIntermediatePageShowUp: true, + shouldIntermediatePageShowUp: payload !== "DEFAULT", }), ); setPendingIntegrationGoto(link.goto); From a65da49d97cac116cf814116243b751327e12822 Mon Sep 17 00:00:00 2001 From: alashchev17 Date: Wed, 11 Dec 2024 16:23:24 +0100 Subject: [PATCH 5/5] chore: type annotation for Chat.stories meta, instead of satisfies --- src/components/Chat/Chat.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Chat/Chat.stories.tsx b/src/components/Chat/Chat.stories.tsx index 04d12da3..fca75ad1 100644 --- a/src/components/Chat/Chat.stories.tsx +++ b/src/components/Chat/Chat.stories.tsx @@ -129,7 +129,7 @@ const Template: React.FC<{ ); }; -const meta = { +const meta: Meta = { title: "Chat", component: Template, parameters: { @@ -145,7 +145,7 @@ const meta = { }, }, argTypes: {}, -} satisfies Meta; +}; export default meta;