Skip to content

Commit d3f0028

Browse files
authored
feat(hiveChat): implement workspace validation and GitHub PAT verification (#1546)
1 parent c34119b commit d3f0028

File tree

4 files changed

+200
-11
lines changed

4 files changed

+200
-11
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"@typescript-eslint/eslint-plugin": "^5.57.1",
6464
"@typescript-eslint/parser": "^5.57.1",
6565
"add": "^2.0.6",
66+
"axios": "^1.10.0",
6667
"bech32": "^2.0.0",
6768
"bootstrap": "^4.5.0",
6869
"clsx": "^2.0.0",

src/people/hiveChat/ChatSplashScreen.tsx

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
2-
import { useState } from 'react';
32
import styled from 'styled-components';
3+
import { Link } from 'react-router-dom';
44

55
interface User {
66
alias: string;
@@ -9,6 +9,10 @@ interface User {
99
interface SplashScreenProps {
1010
user: User;
1111
onSendMessage: (message: string) => void;
12+
isWorkspaceIncomplete?: boolean;
13+
isPATExpired?: boolean;
14+
workspaceUuid?: string;
15+
disableInput?: boolean;
1216
}
1317

1418
const SplashScreenContainer = styled.div`
@@ -39,15 +43,72 @@ const WelcomeHeader = styled.h1`
3943
const WelcomeTagline = styled.p`
4044
font-size: 1.2rem;
4145
color: #64748b;
42-
margin-bottom: 2.5rem;
4346
line-height: 1.6;
4447
max-width: 600px;
4548
`;
4649

47-
const SplashScreen: React.FC<SplashScreenProps> = ({ user, onSendMessage }) => {
48-
const [visible, setVisible] = useState(true);
50+
const RequirementsList = styled.ul`
51+
text-align: left;
52+
width: fit-content;
53+
color: #64748b;
54+
font-size: 1.1rem;
55+
line-height: 1.8;
56+
`;
57+
58+
const RequirementItem = styled.li`
59+
margin-bottom: 0.1px;
60+
`;
61+
62+
const SettingsLink = styled(Link)`
63+
display: inline-block;
64+
padding: 10px 24px;
65+
margin-left: 20px;
66+
background-color: #4285f4;
67+
color: white;
68+
text-decoration: none;
69+
border-radius: 8px;
70+
font-weight: 500;
71+
transition: all 0.2s;
72+
73+
&:hover {
74+
color: black;
75+
background-color: #3367d6;
76+
transform: translateY(-1px);
77+
text-decoration: none;
78+
}
79+
`;
80+
81+
const SplashScreen: React.FC<SplashScreenProps> = ({
82+
user,
83+
isWorkspaceIncomplete,
84+
isPATExpired,
85+
workspaceUuid
86+
}) => {
87+
if (isWorkspaceIncomplete || isPATExpired) {
88+
return (
89+
<SplashScreenContainer>
90+
<WelcomeHeader>Workspace Incomplete</WelcomeHeader>
91+
<WelcomeTagline>
92+
{isPATExpired
93+
? 'Your GitHub Personal Access Token (PAT) has expired. Please update it in Settings.'
94+
: 'To get started, please complete your workspace setup in the settings.'}
95+
</WelcomeTagline>
96+
97+
{!isPATExpired && (
98+
<>
99+
<WelcomeTagline>You need to provide:</WelcomeTagline>
100+
<RequirementsList>
101+
<RequirementItem>Code Space URL</RequirementItem>
102+
<RequirementItem>Code Graph URL</RequirementItem>
103+
<RequirementItem>Secret Alias</RequirementItem>
104+
</RequirementsList>
105+
</>
106+
)}
49107

50-
if (!visible) return null;
108+
<SettingsLink to={`/workspace/${workspaceUuid}/mission`}>Go to Settings</SettingsLink>
109+
</SplashScreenContainer>
110+
);
111+
}
51112

52113
return (
53114
<SplashScreenContainer>

src/people/hiveChat/index.tsx

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { ModelOption } from './modelSelector.tsx';
2727
import { ActionArtifactRenderer } from './ActionArtifactRenderer';
2828
import ChatStatusDisplay from './ChatStatusDisplay.tsx';
2929
import StaktrakRecorder from './StaktrakRecorder';
30-
30+
import axios from 'axios';
3131
import SplashScreen from './ChatSplashScreen';
3232

3333
interface RouteParams {
@@ -640,6 +640,10 @@ export const HiveChatView: React.FC = observer(() => {
640640
const [sseLogs, setSseLogs] = useState<SSEMessage[]>([]);
641641
const [, setStaktrakReady] = useState(false);
642642
const [iframeUrl, setIframeUrl] = useState('');
643+
const [isWorkspaceComplete, setIsWorkspaceComplete] = useState<boolean>(false);
644+
const [isPATExpired, setIsPATExpired] = useState<boolean>(false);
645+
const [isValidating, setIsValidating] = useState<boolean>(true);
646+
const [workspaceData, setWorkspaceData] = useState<any>(null);
643647
useBrowserTabTitle('Hive Chat');
644648

645649
if (isVerboseLoggingEnabled) {
@@ -791,6 +795,19 @@ export const HiveChatView: React.FC = observer(() => {
791795
const messageText = messageToSend || message;
792796
if (!messageText.trim() || isSending) return;
793797

798+
if (!isWorkspaceComplete || isPATExpired) {
799+
ui.setToasts([
800+
{
801+
title: 'Cannot Send Message',
802+
color: 'danger',
803+
text: isPATExpired
804+
? 'Your GitHub PAT has expired. Please update it in settings.'
805+
: 'Your workspace setup is incomplete. Please check settings.'
806+
}
807+
]);
808+
return;
809+
}
810+
794811
setIsSending(true);
795812
try {
796813
if (messageText.includes('@Testing')) {
@@ -1341,6 +1358,18 @@ export const HiveChatView: React.FC = observer(() => {
13411358
(textArtifact && textArtifact.length > 0);
13421359

13431360
const handleSplashMessage = async (msg: string) => {
1361+
if (!isWorkspaceComplete || isPATExpired) {
1362+
ui.setToasts([
1363+
{
1364+
title: 'Cannot Send Message',
1365+
color: 'danger',
1366+
text: isPATExpired
1367+
? 'Your GitHub PAT has expired. Please update it in settings.'
1368+
: 'Your workspace setup is incomplete. Please check settings.'
1369+
}
1370+
]);
1371+
return;
1372+
}
13441373
setMessage(msg);
13451374
setIsSending(true);
13461375

@@ -1835,7 +1864,76 @@ export const HiveChatView: React.FC = observer(() => {
18351864
}
18361865
}, [isTestingMode, artifactTab]);
18371866

1838-
if (loading) {
1867+
const validateGitHubPAT = async (pat: string): Promise<boolean> => {
1868+
if (!pat) return false;
1869+
1870+
try {
1871+
const response = await axios.get('https://api.github.com/user', {
1872+
headers: {
1873+
Authorization: `token ${pat}`
1874+
}
1875+
});
1876+
1877+
return response.status === 200;
1878+
} catch (error) {
1879+
console.error('Error validating GitHub PAT:', error);
1880+
return false;
1881+
}
1882+
};
1883+
1884+
const checkWorkspaceSetup = useCallback(async () => {
1885+
if (!uuid) return;
1886+
1887+
setIsValidating(true);
1888+
try {
1889+
const workspace = await main.getUserWorkspaceByUuid(uuid);
1890+
setWorkspaceData(workspace);
1891+
1892+
if (!workspace) {
1893+
setIsWorkspaceComplete(false);
1894+
setIsValidating(false);
1895+
return;
1896+
}
1897+
1898+
const codeGraph = await main.getWorkspaceCodeGraph(uuid);
1899+
1900+
const hasCodeGraphUrl = codeGraph && codeGraph.url;
1901+
const hasSecretAlias = codeGraph && codeGraph.secret_alias;
1902+
1903+
let patValid = false;
1904+
const codeSpaceConfig = await main.getCodeSpaceConfig(uuid);
1905+
1906+
const hasCodespaceUrl = codeSpaceConfig && codeSpaceConfig.url;
1907+
const hasUsername = codeSpaceConfig && codeSpaceConfig.username;
1908+
const hasPAT = codeSpaceConfig && codeSpaceConfig.pat;
1909+
1910+
if (hasPAT) {
1911+
patValid = await validateGitHubPAT(codeSpaceConfig.pat);
1912+
setIsPATExpired(!!(!patValid && hasPAT));
1913+
}
1914+
1915+
const isComplete = !!(
1916+
hasCodeGraphUrl &&
1917+
hasSecretAlias &&
1918+
hasCodespaceUrl &&
1919+
hasUsername &&
1920+
hasPAT &&
1921+
patValid
1922+
);
1923+
setIsWorkspaceComplete(isComplete);
1924+
} catch (error) {
1925+
console.error('Error checking workspace setup:', error);
1926+
setIsWorkspaceComplete(false);
1927+
} finally {
1928+
setIsValidating(false);
1929+
}
1930+
}, [uuid, main]);
1931+
1932+
useEffect(() => {
1933+
checkWorkspaceSetup();
1934+
}, [checkWorkspaceSetup]);
1935+
1936+
if (loading || isValidating) {
18391937
return (
18401938
<Container collapsed={collapsed} ref={containerRef}>
18411939
<LoadingContainer>
@@ -1902,6 +2000,10 @@ export const HiveChatView: React.FC = observer(() => {
19022000
<SplashScreen
19032001
user={{ alias: ui.meInfo?.owner_alias || 'User' }}
19042002
onSendMessage={handleSplashMessage}
2003+
isWorkspaceIncomplete={!isWorkspaceComplete}
2004+
isPATExpired={isPATExpired}
2005+
workspaceUuid={uuid}
2006+
disableInput={!isWorkspaceComplete || isPATExpired}
19052007
/>
19062008
</SplashContainer>
19072009
)}
@@ -1975,18 +2077,25 @@ export const HiveChatView: React.FC = observer(() => {
19752077
value={message}
19762078
onChange={handleMessageChange}
19772079
onKeyPress={handleKeyPress}
1978-
placeholder="Type your message..."
1979-
disabled={isSending}
2080+
placeholder={
2081+
!isWorkspaceComplete || isPATExpired
2082+
? 'Complete workspace setup to send messages'
2083+
: 'Type your message...'
2084+
}
2085+
disabled={isSending || !isWorkspaceComplete || isPATExpired}
19802086
/>
19812087
{isPdfUploadEnabled && (
1982-
<AttachButton onClick={() => setIsUploadModalOpen(true)} disabled={isSending}>
2088+
<AttachButton
2089+
onClick={() => setIsUploadModalOpen(true)}
2090+
disabled={isSending || !isWorkspaceComplete || isPATExpired}
2091+
>
19832092
Attach
19842093
<AttachIcon icon="attach_file" />
19852094
</AttachButton>
19862095
)}
19872096
<SendButton
19882097
onClick={() => handleSendMessage()}
1989-
disabled={!message.trim() || isSending}
2098+
disabled={!message.trim() || isSending || !isWorkspaceComplete || isPATExpired}
19902099
>
19912100
Send
19922101
</SendButton>

src/store/main.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5328,6 +5328,24 @@ export class MainStore {
53285328
return null;
53295329
}
53305330
}
5331+
5332+
async getCodeSpaceConfig(
5333+
workspace_uuid: string
5334+
): Promise<{ url: string; username: string; pat: string } | null> {
5335+
try {
5336+
const response = await this.getCodeSpace(workspace_uuid);
5337+
if (!response) return null;
5338+
5339+
return {
5340+
url: response.codeSpaceURL || '',
5341+
username: response.username || '',
5342+
pat: response.githubPat || ''
5343+
};
5344+
} catch (e) {
5345+
console.log('Error getting code space config', e);
5346+
return null;
5347+
}
5348+
}
53315349
}
53325350

53335351
export const mainStore = new MainStore();

0 commit comments

Comments
 (0)